Compare commits
54 Commits
main
...
local-test
Author | SHA1 | Date | |
---|---|---|---|
|
e502f5b6a5 | ||
|
7a64171b96 | ||
|
8791aee96c | ||
|
61de18077e | ||
|
25a63b1fb7 | ||
|
15df0ca22c | ||
|
5ce122af28 | ||
|
2ac74d5d76 | ||
|
f90e4048ef | ||
|
50ed464ff2 | ||
|
505230b26c | ||
|
2e1eecfdad | ||
|
4d8c0e0bad | ||
|
7226904e04 | ||
|
3798572b0c | ||
|
176d9b09e4 | ||
|
18207bee18 | ||
|
f70e08e5e8 | ||
|
2d6d74d41e | ||
|
255301509a | ||
|
aae184e891 | ||
|
a3994f0218 | ||
|
bf9b3f501c | ||
|
c52a6c813c | ||
|
54adab0e21 | ||
|
29d249c33b | ||
|
a0967e7583 | ||
|
c1a4a3701e | ||
|
ef38fe0566 | ||
|
930dbda334 | ||
|
e8fa4d119c | ||
|
df4c9940f3 | ||
|
6ddc4fd110 | ||
|
ed45a08ad7 | ||
|
b64200284a | ||
|
957946a822 | ||
|
d9b8b38590 | ||
|
50a0799f28 | ||
|
2a7a2ffc99 | ||
|
875788a71d | ||
|
e0169dfc06 | ||
|
b868507bb0 | ||
|
eb239b659a | ||
|
82ce88a92e | ||
|
8350c6600f | ||
|
83d4020d61 | ||
|
8b59751038 | ||
|
41614d2c2b | ||
|
b6d8aea3a9 | ||
|
5b570cd036 | ||
|
d27bfdda08 | ||
|
c1b2e70989 | ||
|
d50404736f | ||
|
2ac1c1b826 |
@ -7,18 +7,20 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-rust:
|
build-rust:
|
||||||
runs-on: joylink-local233
|
runs-on: joylink-local233-rust
|
||||||
steps:
|
steps:
|
||||||
- name: 检出代码
|
- name: 检出代码
|
||||||
uses: https://gitea.joylink.club/actions/checkout@v4
|
uses: https://gitea.joylink.club/actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Install Rust
|
- uses: https://gitea.joylink.club/actions/rust-cache@v2
|
||||||
|
with:
|
||||||
|
cache-on-failure: true
|
||||||
|
cache-all-crates: true
|
||||||
|
- name: Install musl-tools
|
||||||
|
run: apt-get update && apt-get install -y musl-tools
|
||||||
|
- name: build Release
|
||||||
run: |
|
run: |
|
||||||
export RUSTUP_DIST_SERVER="https://rsproxy.cn"
|
|
||||||
export RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup"
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh
|
|
||||||
mkdir -p ~/.cargo
|
|
||||||
echo '[source.crates-io]' > ~/.cargo/config.toml
|
echo '[source.crates-io]' > ~/.cargo/config.toml
|
||||||
echo 'replace-with = "rsproxy-sparse"' >> ~/.cargo/config.toml
|
echo 'replace-with = "rsproxy-sparse"' >> ~/.cargo/config.toml
|
||||||
echo '[source.rsproxy]' >> ~/.cargo/config.toml
|
echo '[source.rsproxy]' >> ~/.cargo/config.toml
|
||||||
@ -29,10 +31,9 @@ jobs:
|
|||||||
echo 'index = "https://rsproxy.cn/crates.io-index"' >> ~/.cargo/config.toml
|
echo 'index = "https://rsproxy.cn/crates.io-index"' >> ~/.cargo/config.toml
|
||||||
echo '[net]' >> ~/.cargo/config.toml
|
echo '[net]' >> ~/.cargo/config.toml
|
||||||
echo 'git-fetch-with-cli = true' >> ~/.cargo/config.toml
|
echo 'git-fetch-with-cli = true' >> ~/.cargo/config.toml
|
||||||
cat ~/.cargo/config.toml
|
cat $HOME/.cargo/config.toml
|
||||||
- uses: https://gitea.joylink.club/actions/rust-cache@v2
|
. ~/.bashrc
|
||||||
- name: build Release
|
echo $PATH
|
||||||
run: |
|
|
||||||
rustup target add x86_64-unknown-linux-musl
|
rustup target add x86_64-unknown-linux-musl
|
||||||
cargo build --release --target x86_64-unknown-linux-musl
|
cargo build --release --target x86_64-unknown-linux-musl
|
||||||
- name: 设置 Docker
|
- name: 设置 Docker
|
||||||
@ -49,10 +50,10 @@ jobs:
|
|||||||
uses: https://gitea.joylink.club/docker/build-push-action@v5
|
uses: https://gitea.joylink.club/docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile-manager
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
gitea.joylink.club/joylink/rtss-simulation:local-test
|
gitea.joylink.club/joylink/rtsam:lt
|
||||||
- name: 发布到本地测试环境
|
- name: 发布到本地测试环境
|
||||||
uses: https://gitea.joylink.club/appleboy/ssh-action@v1.0.3
|
uses: https://gitea.joylink.club/appleboy/ssh-action@v1.0.3
|
||||||
with:
|
with:
|
||||||
@ -61,6 +62,6 @@ jobs:
|
|||||||
username: ${{ secrets.LOCAL_233_SSH_USER }}
|
username: ${{ secrets.LOCAL_233_SSH_USER }}
|
||||||
password: ${{ secrets.LOCAL_233_SSH_PASSWORD }}
|
password: ${{ secrets.LOCAL_233_SSH_PASSWORD }}
|
||||||
script: |
|
script: |
|
||||||
docker rm -f rtss-simulation || echo "rtss-simulation not exist"
|
docker rm -f rtsa-manage || echo "rtsa-manage not exist"
|
||||||
docker pull gitea.joylink.club/joylink/rtss-simulation:local-test
|
docker pull gitea.joylink.club/joylink/rtsam:lt
|
||||||
docker run --name rtss-simulation --restart=always -e RUN_MODE=local-test --network net --ip 10.11.11.11 -d -p 8765:8765 -v /usr/local/joylink/logs/simulation:/logs/simulation gitea.joylink.club/joylink/rtss-simulation:local-test
|
docker run --name rtsa-manage --restart=always -e RUN_MODE=local_test --network jlnet --ip 10.11.11.11 -d -p 8765:8765 -v /usr/local/joylink/logs/simulation:/logs/simulation gitea.joylink.club/joylink/rtsam:lt
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
.env
|
||||||
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -1,3 +1,3 @@
|
|||||||
[submodule "rtss-proto-msg"]
|
[submodule "rtsa-proto-msg"]
|
||||||
path = rtss-proto-msg
|
path = rtsa-proto-msg
|
||||||
url = https://gitea.joylink.club/joylink/rtss-proto-msg.git
|
url = https://gitea.joylink.club/joylink/rtsa-proto-msg.git
|
||||||
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug",
|
||||||
|
"program": "${workspaceFolder}/<executable file>",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@ -2,6 +2,8 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"chrono",
|
"chrono",
|
||||||
"cpus",
|
"cpus",
|
||||||
|
"dashmap",
|
||||||
|
"eventloop",
|
||||||
"Graphi",
|
"Graphi",
|
||||||
"graphiql",
|
"graphiql",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
@ -11,13 +13,23 @@
|
|||||||
"Joylink",
|
"Joylink",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"mplj",
|
"mplj",
|
||||||
|
"Mqtt",
|
||||||
|
"mqttbytes",
|
||||||
|
"Neng",
|
||||||
|
"nextval",
|
||||||
|
"oneshot",
|
||||||
|
"otype",
|
||||||
"plpgsql",
|
"plpgsql",
|
||||||
"prost",
|
"prost",
|
||||||
"proto",
|
"proto",
|
||||||
"protoc",
|
"protoc",
|
||||||
"protos",
|
"protos",
|
||||||
"repr",
|
"repr",
|
||||||
"rtss",
|
"reqwest",
|
||||||
|
"rtsa",
|
||||||
|
"rtsa",
|
||||||
|
"rumqtt",
|
||||||
|
"rumqttc",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
827
Cargo.lock
generated
827
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@ -1,12 +1,7 @@
|
|||||||
[package]
|
|
||||||
name = "rtss_simulation"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/*"]
|
members = ["crates/*", "manager", "simulation"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
bevy_app = "0.14"
|
bevy_app = "0.14"
|
||||||
@ -14,24 +9,22 @@ bevy_core = "0.14"
|
|||||||
bevy_ecs = "0.14"
|
bevy_ecs = "0.14"
|
||||||
bevy_time = "0.14"
|
bevy_time = "0.14"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
tokio = { version = "1.40", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.41.0", features = ["macros", "rt-multi-thread"] }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0.65"
|
||||||
sqlx = { version = "0.8", features = [
|
sqlx = { version = "0.8", features = [
|
||||||
"runtime-tokio",
|
"runtime-tokio",
|
||||||
"postgres",
|
"postgres",
|
||||||
"json",
|
"json",
|
||||||
"chrono",
|
"chrono",
|
||||||
] }
|
] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
anyhow = "1.0"
|
serde_json = "1.0.132"
|
||||||
|
anyhow = "1.0.91"
|
||||||
[dependencies]
|
async-trait = "0.1.83"
|
||||||
tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread"] }
|
bytes = "1.8.0"
|
||||||
rtss_log = { path = "crates/rtss_log" }
|
lazy_static = "1.5.0"
|
||||||
rtss_api = { path = "crates/rtss_api" }
|
config = "0.14.1"
|
||||||
rtss_db = { path = "crates/rtss_db" }
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
serde = { workspace = true }
|
|
||||||
config = "0.14.0"
|
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
|
||||||
enum_dispatch = "0.3"
|
enum_dispatch = "0.3"
|
||||||
anyhow = { workspace = true }
|
tower = { version = "0.5.1", features = ["util"] }
|
||||||
|
regex = "1.11.1"
|
||||||
|
12
Dockerfile
12
Dockerfile
@ -1,12 +0,0 @@
|
|||||||
FROM alpine
|
|
||||||
|
|
||||||
ENV RUN_MODE=prod
|
|
||||||
|
|
||||||
WORKDIR /rtss_sim
|
|
||||||
COPY ./target/x86_64-unknown-linux-musl/release/rtss_simulation ./rtss_sim
|
|
||||||
COPY ./conf/* ./conf/
|
|
||||||
COPY ./migrations/* ./migrations/
|
|
||||||
|
|
||||||
EXPOSE 8765
|
|
||||||
|
|
||||||
CMD ["sh", "-c", "./rtss_sim db migrate && ./rtss_sim serve"]
|
|
21
Dockerfile-manager
Normal file
21
Dockerfile-manager
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
FROM alpine
|
||||||
|
|
||||||
|
ENV RUN_MODE=prod
|
||||||
|
|
||||||
|
# 安装 tzdata 包
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
|
||||||
|
# 设置时区环境变量,例如设置为 Asia/Shanghai
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# 复制时区信息到系统时区目录
|
||||||
|
RUN cp /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
WORKDIR /rtsa
|
||||||
|
COPY ./target/x86_64-unknown-linux-musl/release/manager ./rtsa_m
|
||||||
|
COPY ./manager/conf/* ./conf/
|
||||||
|
COPY ./migrations/* ./migrations/
|
||||||
|
|
||||||
|
EXPOSE 8765
|
||||||
|
|
||||||
|
CMD ["sh", "-c", "./rtsa_m db migrate && ./rtsa_m serve"]
|
@ -1,4 +1,4 @@
|
|||||||
[default.extend-words]
|
[default.extend-words]
|
||||||
|
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = ["README.md", "rtss-proto-msg/*"]
|
extend-exclude = ["README.md", "rtsa-proto-msg/*"]
|
||||||
|
27
crates/rtsa_db/Cargo.toml
Normal file
27
crates/rtsa_db/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "rtsa_db"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
sqlx = { workspace = true, features = [
|
||||||
|
"runtime-tokio",
|
||||||
|
"macros",
|
||||||
|
"chrono",
|
||||||
|
"json",
|
||||||
|
"derive",
|
||||||
|
"postgres",
|
||||||
|
"uuid",
|
||||||
|
] }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
lazy_static = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
|
argon2 = "0.5.3"
|
||||||
|
rand_core = { version = "0.6.4", features = ["std"] }
|
||||||
|
md-5 = "0.10.6"
|
||||||
|
|
||||||
|
rtsa_dto = { path = "../rtsa_dto" }
|
||||||
|
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,29 +1,34 @@
|
|||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
use rtss_dto::common::DataType;
|
use rtsa_log::tracing::debug;
|
||||||
use rtss_log::tracing::debug;
|
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::RtssDbAccessor;
|
use super::RtsaDbAccessor;
|
||||||
|
|
||||||
/// 草稿数据管理
|
/// 草稿数据管理
|
||||||
#[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<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+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: i32,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<bool, DbAccessError>;
|
||||||
/// 创建草稿数据基本信息
|
/// 创建草稿数据基本信息
|
||||||
async fn create_draft_data(
|
async fn create_draft_data(
|
||||||
&self,
|
&self,
|
||||||
@ -41,6 +46,12 @@ pub trait DraftDataAccessor {
|
|||||||
id: i32,
|
id: i32,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> Result<DraftDataModel, DbAccessError>;
|
) -> Result<DraftDataModel, DbAccessError>;
|
||||||
|
/// 设置草稿数据共享
|
||||||
|
async fn set_draft_data_shared(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
is_shared: bool,
|
||||||
|
) -> Result<DraftDataModel, DbAccessError>;
|
||||||
/// 删除草稿数据
|
/// 删除草稿数据
|
||||||
async fn delete_draft_data(&self, id: &[i32]) -> Result<(), DbAccessError>;
|
async fn delete_draft_data(&self, id: &[i32]) -> Result<(), DbAccessError>;
|
||||||
/// 设置默认发布数据id
|
/// 设置默认发布数据id
|
||||||
@ -62,10 +73,16 @@ 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 is_shared: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DraftDataQuery {
|
impl DraftDataQuery {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_user_id(mut self, user_id: i32) -> Self {
|
pub fn with_user_id(mut self, user_id: i32) -> Self {
|
||||||
self.user_id = Some(user_id);
|
self.user_id = Some(user_id);
|
||||||
self
|
self
|
||||||
@ -76,11 +93,26 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_is_shared(mut self, is_shared: bool) -> Self {
|
||||||
|
self.is_shared = Some(is_shared);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_option_options(mut self, options: Option<Value>) -> Self {
|
||||||
|
self.options = options;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_options(mut self, options: Value) -> Self {
|
||||||
|
self.options = Some(options);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn build_filter(&self) -> String {
|
fn build_filter(&self) -> String {
|
||||||
let mut filters = vec![];
|
let mut filters = vec![];
|
||||||
if let Some(user_id) = self.user_id {
|
if let Some(user_id) = self.user_id {
|
||||||
@ -97,7 +129,21 @@ 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 {
|
||||||
|
filters.push(format!(
|
||||||
|
"{} = {}",
|
||||||
|
DraftDataColumn::IsShared.name(),
|
||||||
|
is_shared
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(options) = &self.options {
|
||||||
|
filters.push(format!(
|
||||||
|
"{} @> '{}'",
|
||||||
|
DraftDataColumn::Options.name(),
|
||||||
|
serde_json::to_string(options).unwrap()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if filters.is_empty() {
|
if filters.is_empty() {
|
||||||
@ -110,40 +156,60 @@ impl DraftDataQuery {
|
|||||||
|
|
||||||
pub struct CreateDraftData {
|
pub struct CreateDraftData {
|
||||||
name: String,
|
name: String,
|
||||||
data_type: DataType,
|
data_type: i32,
|
||||||
|
options: Option<Value>,
|
||||||
data: Option<Vec<u8>>,
|
data: Option<Vec<u8>>,
|
||||||
default_release_data_id: Option<i32>,
|
default_release_data_id: Option<i32>,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
options: None,
|
||||||
data: None,
|
data: None,
|
||||||
default_release_data_id: None,
|
default_release_data_id: None,
|
||||||
user_id,
|
user_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_option_options(mut self, options: Option<Value>) -> Self {
|
||||||
|
self.options = options;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_options(mut self, options: Value) -> Self {
|
||||||
|
self.options = Some(options);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_data(mut self, data: &[u8]) -> Self {
|
pub fn with_data(mut self, data: &[u8]) -> Self {
|
||||||
self.data = Some(data.to_vec());
|
self.data = Some(data.to_vec());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_option_default_release_data_id(
|
||||||
|
mut self,
|
||||||
|
default_release_data_id: Option<i32>,
|
||||||
|
) -> Self {
|
||||||
|
self.default_release_data_id = default_release_data_id;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_default_release_data_id(mut self, default_release_data_id: i32) -> Self {
|
pub fn with_default_release_data_id(mut self, default_release_data_id: i32) -> Self {
|
||||||
self.default_release_data_id = Some(default_release_data_id);
|
self.default_release_data_id = Some(default_release_data_id);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DraftDataAccessor for RtssDbAccessor {
|
impl DraftDataAccessor for RtsaDbAccessor {
|
||||||
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<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}");
|
||||||
@ -153,15 +219,18 @@ impl DraftDataAccessor for RtssDbAccessor {
|
|||||||
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!(
|
||||||
"{}, {}, {}, {}, {}, {}",
|
"{}, {}, {}, {}, {}, {}, {}, {}, {}",
|
||||||
DraftDataColumn::Id.name(),
|
DraftDataColumn::Id.name(),
|
||||||
DraftDataColumn::Name.name(),
|
DraftDataColumn::Name.name(),
|
||||||
DraftDataColumn::DataType.name(),
|
DraftDataColumn::DataType.name(),
|
||||||
|
DraftDataColumn::Options.name(),
|
||||||
DraftDataColumn::UserId.name(),
|
DraftDataColumn::UserId.name(),
|
||||||
|
DraftDataColumn::DefaultReleaseDataId.name(),
|
||||||
|
DraftDataColumn::IsShared.name(),
|
||||||
DraftDataColumn::CreatedAt.name(),
|
DraftDataColumn::CreatedAt.name(),
|
||||||
DraftDataColumn::UpdatedAt.name(),
|
DraftDataColumn::UpdatedAt.name(),
|
||||||
);
|
);
|
||||||
@ -175,7 +244,7 @@ impl DraftDataAccessor for RtssDbAccessor {
|
|||||||
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> {
|
||||||
@ -186,19 +255,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: i32,
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -209,29 +284,33 @@ 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(
|
||||||
|
"uid + data_type + name".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// 插入数据
|
// 插入数据
|
||||||
let table = DraftDataColumn::Table.name();
|
let table = DraftDataColumn::Table.name();
|
||||||
let columns = format!(
|
let columns = format!(
|
||||||
"{}, {}, {}, {}, {}",
|
"{}, {}, {}, {}, {}, {}",
|
||||||
DraftDataColumn::Name.name(),
|
DraftDataColumn::Name.name(),
|
||||||
DraftDataColumn::DataType.name(),
|
DraftDataColumn::DataType.name(),
|
||||||
|
DraftDataColumn::Options.name(),
|
||||||
DraftDataColumn::UserId.name(),
|
DraftDataColumn::UserId.name(),
|
||||||
DraftDataColumn::Data.name(),
|
DraftDataColumn::Data.name(),
|
||||||
DraftDataColumn::DefaultReleaseDataId.name(),
|
DraftDataColumn::DefaultReleaseDataId.name(),
|
||||||
);
|
);
|
||||||
let sql =
|
let sql =
|
||||||
format!("INSERT INTO {table} ({columns}) VALUES ($1, $2, $3, $4, $5) RETURNING *",);
|
format!("INSERT INTO {table} ({columns}) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *",);
|
||||||
// log sql
|
// log sql
|
||||||
debug!("create sql: {}", sql);
|
debug!("create sql: {}", sql);
|
||||||
// 插入数据
|
// 插入数据
|
||||||
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.user_id)
|
.bind(create.user_id)
|
||||||
.bind(create.data)
|
.bind(create.data)
|
||||||
.bind(create.default_release_data_id)
|
.bind(create.default_release_data_id)
|
||||||
@ -283,6 +362,27 @@ impl DraftDataAccessor for RtssDbAccessor {
|
|||||||
Ok(draft_data)
|
Ok(draft_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn set_draft_data_shared(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
is_shared: bool,
|
||||||
|
) -> Result<DraftDataModel, DbAccessError> {
|
||||||
|
let table = DraftDataColumn::Table.name();
|
||||||
|
let is_shared_column = DraftDataColumn::IsShared.name();
|
||||||
|
let updated_at_column = DraftDataColumn::UpdatedAt.name();
|
||||||
|
let id_column = DraftDataColumn::Id.name();
|
||||||
|
let sql = format!("UPDATE {table} SET {is_shared_column} = $1, {updated_at_column} = 'now()' WHERE {id_column} = $2 RETURNING *",);
|
||||||
|
// log sql
|
||||||
|
debug!("set shared sql: {}", sql);
|
||||||
|
let draft_data = sqlx::query_as(&sql)
|
||||||
|
.bind(is_shared)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(draft_data)
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete_draft_data(&self, ids: &[i32]) -> Result<(), DbAccessError> {
|
async fn delete_draft_data(&self, ids: &[i32]) -> Result<(), DbAccessError> {
|
||||||
let table = DraftDataColumn::Table.name();
|
let table = DraftDataColumn::Table.name();
|
||||||
let id_column = DraftDataColumn::Id.name();
|
let id_column = DraftDataColumn::Id.name();
|
||||||
@ -323,30 +423,87 @@ 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_data(draft_data.data.as_ref().unwrap());
|
|
||||||
self.create_draft_data(create).await
|
self.create_draft_data(create).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::{RegisterUser, UserAccessor};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use rtss_log::tracing::Level;
|
use rtsa_log::tracing::Level;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
// 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> {
|
||||||
rtss_log::Logging::default().with_level(Level::DEBUG).init();
|
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
||||||
let accessor = crate::db_access::RtssDbAccessor::new(pool);
|
let accessor = crate::db_access::RtsaDbAccessor::new(pool);
|
||||||
|
// 注册10个用户
|
||||||
|
for i in 0..10 {
|
||||||
|
let user = RegisterUser::new(&format!("test{}", i), "123456");
|
||||||
|
accessor.register_user(user).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);
|
||||||
@ -355,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");
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@ -384,12 +541,21 @@ mod tests {
|
|||||||
println!("{:?}", get_by_id);
|
println!("{:?}", get_by_id);
|
||||||
assert!(get_by_id.data.unwrap() == data);
|
assert!(get_by_id.data.unwrap() == data);
|
||||||
|
|
||||||
|
// set shared测试
|
||||||
|
let get_by_id = accessor.set_draft_data_shared(res.id, true).await?;
|
||||||
|
assert!(get_by_id.is_shared);
|
||||||
|
|
||||||
// save as new draft测试
|
// save as new draft测试
|
||||||
let new_draft = accessor.save_as_new_draft(res.id, "new draft", 11).await?;
|
let new_draft = accessor.save_as_new_draft(res.id, "new draft", 9).await?;
|
||||||
println!("{:?}", new_draft);
|
println!("{:?}", new_draft);
|
||||||
assert_eq!(new_draft.name, "new draft");
|
assert_eq!(new_draft.name, "new draft");
|
||||||
assert_eq!(new_draft.user_id, 11);
|
assert_eq!(new_draft.user_id, 9);
|
||||||
|
assert_eq!(new_draft.options, res.options);
|
||||||
assert_eq!(new_draft.data.unwrap(), data);
|
assert_eq!(new_draft.data.unwrap(), data);
|
||||||
|
assert_eq!(
|
||||||
|
new_draft.default_release_data_id,
|
||||||
|
res.default_release_data_id
|
||||||
|
);
|
||||||
|
|
||||||
// delete测试
|
// delete测试
|
||||||
accessor.delete_draft_data(&[res.id, new_draft.id]).await?;
|
accessor.delete_draft_data(&[res.id, new_draft.id]).await?;
|
||||||
@ -407,14 +573,7 @@ mod tests {
|
|||||||
|
|
||||||
// 查询确认当前数据已删除
|
// 查询确认当前数据已删除
|
||||||
let page = accessor
|
let page = accessor
|
||||||
.query_draft_data(
|
.paging_query_draft_data(DraftDataQuery::new(), PageQuery::new(1, 100))
|
||||||
DraftDataQuery {
|
|
||||||
user_id: None,
|
|
||||||
name: None,
|
|
||||||
data_type: None,
|
|
||||||
},
|
|
||||||
PageQuery::new(1, 100),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(page.total, 0);
|
assert_eq!(page.total, 0);
|
||||||
|
|
||||||
@ -422,36 +581,71 @@ mod tests {
|
|||||||
// 分四个user_id各插入5条数据
|
// 分四个user_id各插入5条数据
|
||||||
for i in 1..5 {
|
for i in 1..5 {
|
||||||
for j in 1..6 {
|
for j in 1..6 {
|
||||||
accessor
|
if i == 1 {
|
||||||
.create_draft_data(CreateDraftData::new(&format!("test{}", j), DataType::Em, i))
|
let draft = accessor
|
||||||
.await?;
|
.create_draft_data(
|
||||||
|
CreateDraftData::new(&format!("test{}", j), DataType::Iscs as i32, i)
|
||||||
|
.with_options(
|
||||||
|
serde_json::to_value(IscsDataOptions {
|
||||||
|
style: IscsStyle::Style1,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
println!("{:?}", draft);
|
||||||
|
accessor.set_draft_data_shared(draft.id, true).await?;
|
||||||
|
} else {
|
||||||
|
accessor
|
||||||
|
.create_draft_data(CreateDraftData::new(
|
||||||
|
&format!("test{}", j),
|
||||||
|
DataType::Em as i32,
|
||||||
|
i,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let page = accessor
|
let page = accessor
|
||||||
.query_draft_data(
|
.paging_query_draft_data(
|
||||||
DraftDataQuery {
|
DraftDataQuery::new()
|
||||||
user_id: Some(1),
|
.with_user_id(2)
|
||||||
name: Some("test".to_string()),
|
.with_name("test".to_string())
|
||||||
data_type: Some(DataType::Em),
|
.with_data_type(DataType::Em as i32),
|
||||||
},
|
|
||||||
PageQuery::new(1, 10),
|
PageQuery::new(1, 10),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(page.total, 5);
|
assert_eq!(page.total, 5);
|
||||||
|
|
||||||
let page = accessor
|
let page = accessor
|
||||||
.query_draft_data(
|
.paging_query_draft_data(DraftDataQuery::new(), PageQuery::new(1, 100))
|
||||||
DraftDataQuery {
|
|
||||||
user_id: None,
|
|
||||||
name: None,
|
|
||||||
data_type: None,
|
|
||||||
},
|
|
||||||
PageQuery::new(1, 100),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(page.total, 20);
|
assert_eq!(page.total, 20);
|
||||||
|
|
||||||
|
// 查询共享数据
|
||||||
|
let page = accessor
|
||||||
|
.paging_query_draft_data(
|
||||||
|
DraftDataQuery::new().with_is_shared(true),
|
||||||
|
PageQuery::new(1, 10),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(page.total, 5);
|
||||||
|
|
||||||
|
// 查询options
|
||||||
|
let page = accessor
|
||||||
|
.paging_query_draft_data(
|
||||||
|
DraftDataQuery::new().with_options(
|
||||||
|
serde_json::to_value(IscsDataOptions {
|
||||||
|
style: IscsStyle::Style1,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
PageQuery::new(1, 10),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(page.total, 5);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
340
crates/rtsa_db/src/db_access/feature.rs
Normal file
340
crates/rtsa_db/src/db_access/feature.rs
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
use rtsa_dto::common::FeatureType;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::{PageData, PageQuery, TableColumn},
|
||||||
|
model::{FeatureColumn, FeatureModel},
|
||||||
|
DbAccessError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::RtsaDbAccessor;
|
||||||
|
|
||||||
|
/// 功能特性管理
|
||||||
|
#[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<PageData<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 RtsaDbAccessor {
|
||||||
|
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<PageData<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(PageData::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(PageData::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 rtsa_log::tracing::Level;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::{RegisterUser, UserAccessor};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_basic_use(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
||||||
|
let accessor = crate::db_access::RtsaDbAccessor::new(pool);
|
||||||
|
// 注册10个用户
|
||||||
|
for i in 0..10 {
|
||||||
|
let user = RegisterUser::new(&format!("test{}", i), "123456");
|
||||||
|
accessor.register_user(user).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!(feature.is_published);
|
||||||
|
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!(feature.is_published);
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
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!(!feature.is_published);
|
||||||
|
// 分页查询测试
|
||||||
|
// 创建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(())
|
||||||
|
}
|
||||||
|
}
|
96
crates/rtsa_db/src/db_access/mod.rs
Normal file
96
crates/rtsa_db/src/db_access/mod.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
mod draft_data;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
pub use draft_data::*;
|
||||||
|
mod release_data;
|
||||||
|
pub use release_data::*;
|
||||||
|
mod user;
|
||||||
|
use rtsa_log::tracing::error;
|
||||||
|
pub use user::*;
|
||||||
|
mod feature;
|
||||||
|
pub use feature::*;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
mod org;
|
||||||
|
pub use org::*;
|
||||||
|
mod org_user;
|
||||||
|
pub use org_user::*;
|
||||||
|
|
||||||
|
use crate::{model::MqttClientIdSeq, DbAccessError};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref RDA: Mutex<Option<RtsaDbAccessor>> = Mutex::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化全局默认数据库访问器
|
||||||
|
pub async fn init_default_db_accessor(url: &str) {
|
||||||
|
if RDA.lock().unwrap().is_some() {
|
||||||
|
error!("数据库访问器已初始化,请勿重复初始化");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let accessor = get_db_accessor(url).await;
|
||||||
|
let mut rda = RDA.lock().unwrap();
|
||||||
|
*rda = Some(accessor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置全局默认数据库访问器
|
||||||
|
pub fn set_default_db_accessor(accessor: RtsaDbAccessor) {
|
||||||
|
if RDA.lock().unwrap().is_some() {
|
||||||
|
error!("数据库访问器已初始化,请勿重复初始化");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut rda = RDA.lock().unwrap();
|
||||||
|
*rda = Some(accessor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取默认数据库访问器
|
||||||
|
pub fn get_default_db_accessor() -> RtsaDbAccessor {
|
||||||
|
let rda = RDA.lock().unwrap();
|
||||||
|
if rda.is_none() {
|
||||||
|
panic!("数据库访问器未初始化");
|
||||||
|
}
|
||||||
|
rda.as_ref().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RtsaDbAccessor {
|
||||||
|
pool: sqlx::PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RtsaDbAccessor {
|
||||||
|
pub fn new(pool: sqlx::PgPool) -> Self {
|
||||||
|
RtsaDbAccessor { pool }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_next_mqtt_client_id(&self) -> Result<i64, DbAccessError> {
|
||||||
|
let seq_name = MqttClientIdSeq::Name.name();
|
||||||
|
let next = sqlx::query_scalar(&format!("SELECT nextval('{}')", seq_name))
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_db_accessor(url: &str) -> RtsaDbAccessor {
|
||||||
|
let pool = sqlx::PgPool::connect(url).await.expect("连接数据库失败");
|
||||||
|
RtsaDbAccessor::new(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rtsa_log::tracing::{self, Level};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
// You could also do `use foo_crate::MIGRATOR` and just refer to it as `MIGRATOR` here.
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_get_mqtt_client_id(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
||||||
|
let accessor = crate::db_access::RtsaDbAccessor::new(pool);
|
||||||
|
for _ in 0..10 {
|
||||||
|
let id = accessor.get_next_mqtt_client_id().await?;
|
||||||
|
tracing::info!("id = {}", id);
|
||||||
|
assert!(id > 0);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
657
crates/rtsa_db/src/db_access/org.rs
Normal file
657
crates/rtsa_db/src/db_access/org.rs
Normal file
@ -0,0 +1,657 @@
|
|||||||
|
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>;
|
||||||
|
/// 更新组织名称
|
||||||
|
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_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
match row {
|
||||||
|
Some(row) => Ok(row),
|
||||||
|
None => Err(DbAccessError::RowNotExist(
|
||||||
|
OrganizationColumn::Table.name().to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
match row {
|
||||||
|
Some(row) => Ok(row),
|
||||||
|
None => Err(DbAccessError::RowNotExist(
|
||||||
|
OrganizationColumn::Table.name().to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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_names(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_names(&ids).await?;
|
||||||
|
assert_eq!(names.len(), 3);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
700
crates/rtsa_db/src/db_access/org_user.rs
Normal file
700
crates/rtsa_db/src/db_access/org_user.rs
Normal file
@ -0,0 +1,700 @@
|
|||||||
|
use serde_json::Value;
|
||||||
|
use sqlx::Postgres;
|
||||||
|
|
||||||
|
use super::RtsaDbAccessor;
|
||||||
|
use super::{OrgAccessor, RegisterUser, UserAccessor};
|
||||||
|
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>;
|
||||||
|
/// 获取组织用户
|
||||||
|
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(
|
||||||
|
&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_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
match org_user {
|
||||||
|
Some(org_user) => Ok(org_user),
|
||||||
|
None => Err(DbAccessError::RowNotExist(
|
||||||
|
OrganizationUserColumn::Table.name().to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::RowNotExist(_))));
|
||||||
|
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_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,8 +1,8 @@
|
|||||||
use rtss_dto::common::DataType;
|
use serde_json::Value;
|
||||||
use sqlx::types::chrono;
|
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,
|
||||||
@ -10,7 +10,7 @@ use crate::{
|
|||||||
DbAccessError,
|
DbAccessError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{CreateDraftData, DraftDataAccessor, RtssDbAccessor};
|
use super::{CreateDraftData, DraftDataAccessor, RtsaDbAccessor};
|
||||||
|
|
||||||
#[allow(async_fn_in_trait)]
|
#[allow(async_fn_in_trait)]
|
||||||
pub trait ReleaseDataAccessor {
|
pub trait ReleaseDataAccessor {
|
||||||
@ -20,36 +20,53 @@ pub trait ReleaseDataAccessor {
|
|||||||
draft_id: i32,
|
draft_id: i32,
|
||||||
name: &str,
|
name: &str,
|
||||||
description: &str,
|
description: &str,
|
||||||
|
user_id: Option<i32>,
|
||||||
) -> Result<(ReleaseDataModel, ReleaseDataVersionModel), DbAccessError>;
|
) -> Result<(ReleaseDataModel, ReleaseDataVersionModel), DbAccessError>;
|
||||||
/// 从草稿发布到已有草稿默认的release data的新version,并使用新version
|
/// 从草稿发布到已有草稿默认的release data的新version,并使用新version
|
||||||
async fn release_to_existing(
|
async fn release_to_existing(
|
||||||
&self,
|
&self,
|
||||||
draft_id: i32,
|
draft_id: i32,
|
||||||
description: &str,
|
description: &str,
|
||||||
|
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,
|
||||||
) -> Result<PageResult<ReleaseDataModel>, DbAccessError>;
|
) -> Result<PageData<ReleaseDataModel>, DbAccessError>;
|
||||||
/// 检查name是否存在
|
/// 检查name是否存在
|
||||||
async fn is_release_data_name_exist(&self, name: &str) -> Result<bool, DbAccessError>;
|
async fn is_release_data_name_exist(
|
||||||
|
&self,
|
||||||
|
data_type: i32,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<bool, DbAccessError>;
|
||||||
/// 查询发布数据
|
/// 查询发布数据
|
||||||
async fn query_release_data_by_id(
|
async fn query_release_data_by_id(
|
||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
) -> Result<ReleaseDataModel, DbAccessError>;
|
) -> Result<ReleaseDataModel, DbAccessError>;
|
||||||
|
/// 根据id列表查询发布数据name
|
||||||
|
async fn query_release_data_names(
|
||||||
|
&self,
|
||||||
|
release_ids: &[i32],
|
||||||
|
) -> Result<Vec<(i32, String)>, DbAccessError>;
|
||||||
/// 查询发布数据所有版本信息
|
/// 查询发布数据所有版本信息
|
||||||
async fn query_release_data_versions(
|
async fn paging_query_release_data_version_list(
|
||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
) -> Result<Vec<ReleaseDataVersionModel>, DbAccessError>;
|
page: PageQuery,
|
||||||
|
) -> Result<PageData<ReleaseDataVersionModel>, DbAccessError>;
|
||||||
/// 根据id查询发布版本数据
|
/// 根据id查询发布版本数据
|
||||||
async fn query_release_data_version_by_id(
|
async fn query_release_data_version_by_id(
|
||||||
&self,
|
&self,
|
||||||
version_id: i32,
|
version_id: i32,
|
||||||
) -> Result<ReleaseDataVersionModel, DbAccessError>;
|
) -> Result<ReleaseDataVersionModel, DbAccessError>;
|
||||||
|
/// 根据id列表查询发布版本数据description
|
||||||
|
async fn query_release_data_version_descriptions(
|
||||||
|
&self,
|
||||||
|
version_ids: &[i32],
|
||||||
|
) -> Result<Vec<(i32, String)>, DbAccessError>;
|
||||||
/// 查询发布数据详情
|
/// 查询发布数据详情
|
||||||
async fn query_release_data_with_used_version(
|
async fn query_release_data_with_used_version(
|
||||||
&self,
|
&self,
|
||||||
@ -62,19 +79,19 @@ pub trait ReleaseDataAccessor {
|
|||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<ReleaseDataModel, DbAccessError>;
|
) -> Result<ReleaseDataModel, DbAccessError>;
|
||||||
/// 上架/下架发布数据
|
/// 上架/下架发布数据
|
||||||
async fn update_release_data_published(
|
async fn set_release_data_published(
|
||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
is_published: bool,
|
is_published: bool,
|
||||||
) -> Result<ReleaseDataModel, DbAccessError>;
|
) -> Result<ReleaseDataModel, DbAccessError>;
|
||||||
/// 设置在使用的版本
|
/// 设置在使用的版本
|
||||||
async fn set_used_version(
|
async fn set_release_data_used_version(
|
||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
version_id: i32,
|
version_id: i32,
|
||||||
) -> Result<ReleaseDataModel, DbAccessError>;
|
) -> Result<ReleaseDataModel, DbAccessError>;
|
||||||
/// 从指定的版本数据创建草稿
|
/// 从指定的版本数据创建草稿
|
||||||
async fn create_draft_from_version(
|
async fn create_draft_from_release_version(
|
||||||
&self,
|
&self,
|
||||||
version_id: i32,
|
version_id: i32,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
@ -91,7 +108,8 @@ 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<rtss_dto::common::DataType>,
|
pub data_type: Option<i32>,
|
||||||
|
pub options: Option<Value>,
|
||||||
pub is_published: Option<bool>,
|
pub is_published: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +125,7 @@ impl ReleaseDataQuery {
|
|||||||
name: None,
|
name: None,
|
||||||
user_id: None,
|
user_id: None,
|
||||||
data_type: None,
|
data_type: None,
|
||||||
|
options: None,
|
||||||
is_published: None,
|
is_published: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,11 +140,21 @@ impl ReleaseDataQuery {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_data_type(mut self, data_type: rtss_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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_option_options(mut self, options: Option<Value>) -> Self {
|
||||||
|
self.options = options;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_options(mut self, options: Value) -> Self {
|
||||||
|
self.options = Some(options);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_is_published(mut self, is_published: bool) -> Self {
|
pub fn with_is_published(mut self, is_published: bool) -> Self {
|
||||||
self.is_published = Some(is_published);
|
self.is_published = Some(is_published);
|
||||||
self
|
self
|
||||||
@ -144,7 +173,14 @@ 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 {
|
||||||
|
let options_column = ReleaseDataColumn::Options.name();
|
||||||
|
filters.push(format!(
|
||||||
|
"{options_column} @> '{}'",
|
||||||
|
serde_json::to_string(options).unwrap()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Some(is_published) = self.is_published {
|
if let Some(is_published) = self.is_published {
|
||||||
filters.push(format!("{rd_is_published} = {is_published}"));
|
filters.push(format!("{rd_is_published} = {is_published}"));
|
||||||
@ -157,56 +193,103 @@ impl ReleaseDataQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReleaseDataAccessor for RtssDbAccessor {
|
pub struct CreateReleaseVersionData {
|
||||||
async fn release_new_from_draft(
|
pub release_data_id: i32,
|
||||||
|
pub options: Option<serde_json::Value>,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub description: String,
|
||||||
|
pub user_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RtsaDbAccessor {
|
||||||
|
async fn insert_release_data_version<'e, 'c: 'e, E>(
|
||||||
&self,
|
&self,
|
||||||
draft_id: i32,
|
data: CreateReleaseVersionData,
|
||||||
name: &str,
|
executor: E,
|
||||||
description: &str,
|
) -> Result<ReleaseDataVersionModel, DbAccessError>
|
||||||
) -> Result<(ReleaseDataModel, ReleaseDataVersionModel), DbAccessError> {
|
where
|
||||||
// 判断发布数据名称是否已存在
|
E: 'e + sqlx::Executor<'c, Database = Postgres>,
|
||||||
if self.is_release_data_name_exist(name).await? {
|
{
|
||||||
return Err(DbAccessError::DataError("发布数据名称已存在".to_string()));
|
|
||||||
}
|
|
||||||
// 开启事务
|
|
||||||
let mut tx = self.pool.begin().await?;
|
|
||||||
// 查询草稿数据
|
|
||||||
let draft = self.query_draft_data_by_id(draft_id).await?;
|
|
||||||
// 创建发布数据
|
|
||||||
let rd_table = ReleaseDataColumn::Table.name();
|
|
||||||
let rd_insert_columns = format!(
|
|
||||||
"({}, {}, {})",
|
|
||||||
ReleaseDataColumn::Name.name(),
|
|
||||||
ReleaseDataColumn::DataType.name(),
|
|
||||||
ReleaseDataColumn::UserId.name(),
|
|
||||||
);
|
|
||||||
let rd_insert_clause =
|
|
||||||
format!("INSERT INTO {rd_table} {rd_insert_columns} VALUES ($1, $2, $3) RETURNING *");
|
|
||||||
let mut rd = sqlx::query_as::<_, ReleaseDataModel>(&rd_insert_clause)
|
|
||||||
.bind(name)
|
|
||||||
.bind(draft.data_type as i32)
|
|
||||||
.bind(draft.user_id)
|
|
||||||
.fetch_one(&mut *tx)
|
|
||||||
.await?;
|
|
||||||
// 创建发布数据版本
|
|
||||||
let rdv_table = ReleaseDataVersionColumn::Table.name();
|
let rdv_table = ReleaseDataVersionColumn::Table.name();
|
||||||
let rdv_insert_columns = format!(
|
let rdv_insert_columns = format!(
|
||||||
"{}, {}, {}, {}",
|
"{}, {}, {}, {}, {}",
|
||||||
ReleaseDataVersionColumn::ReleaseDataId.name(),
|
ReleaseDataVersionColumn::ReleaseDataId.name(),
|
||||||
|
ReleaseDataVersionColumn::Options.name(),
|
||||||
ReleaseDataVersionColumn::Data.name(),
|
ReleaseDataVersionColumn::Data.name(),
|
||||||
ReleaseDataVersionColumn::Description.name(),
|
ReleaseDataVersionColumn::Description.name(),
|
||||||
ReleaseDataVersionColumn::UserId.name(),
|
ReleaseDataVersionColumn::UserId.name(),
|
||||||
);
|
);
|
||||||
let rdv_insert_clause = format!(
|
let rdv_insert_clause = format!(
|
||||||
"INSERT INTO {rdv_table} ({rdv_insert_columns}) VALUES ($1, $2, $3, $4) RETURNING *"
|
"INSERT INTO {rdv_table} ({rdv_insert_columns}) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||||
|
rdv_table = rdv_table,
|
||||||
|
rdv_insert_columns = rdv_insert_columns
|
||||||
);
|
);
|
||||||
let rdv = sqlx::query_as::<_, ReleaseDataVersionModel>(&rdv_insert_clause)
|
let rdv = sqlx::query_as::<_, ReleaseDataVersionModel>(&rdv_insert_clause)
|
||||||
.bind(rd.id)
|
.bind(data.release_data_id)
|
||||||
.bind(draft.data)
|
.bind(data.options)
|
||||||
.bind(description)
|
.bind(data.data)
|
||||||
.bind(draft.user_id)
|
.bind(data.description)
|
||||||
|
.bind(data.user_id)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await?;
|
||||||
|
Ok(rdv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReleaseDataAccessor for RtsaDbAccessor {
|
||||||
|
async fn release_new_from_draft(
|
||||||
|
&self,
|
||||||
|
draft_id: i32,
|
||||||
|
name: &str,
|
||||||
|
description: &str,
|
||||||
|
user_id: Option<i32>,
|
||||||
|
) -> Result<(ReleaseDataModel, ReleaseDataVersionModel), DbAccessError> {
|
||||||
|
// 查询草稿数据
|
||||||
|
let draft = self.query_draft_data_by_id(draft_id).await?;
|
||||||
|
if draft.data.is_none() {
|
||||||
|
return Err(DbAccessError::DataError("草稿数据为空".to_string()));
|
||||||
|
}
|
||||||
|
// 判断发布数据名称是否已存在
|
||||||
|
if self
|
||||||
|
.is_release_data_name_exist(draft.data_type, name)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Err(DbAccessError::DataError("发布数据名称已存在".to_string()));
|
||||||
|
}
|
||||||
|
// 开启事务
|
||||||
|
let mut tx = self.pool.begin().await?;
|
||||||
|
// 创建发布数据
|
||||||
|
let rd_table = ReleaseDataColumn::Table.name();
|
||||||
|
let rd_insert_columns = format!(
|
||||||
|
"({}, {}, {}, {})",
|
||||||
|
ReleaseDataColumn::Name.name(),
|
||||||
|
ReleaseDataColumn::DataType.name(),
|
||||||
|
ReleaseDataColumn::Options.name(),
|
||||||
|
ReleaseDataColumn::UserId.name(),
|
||||||
|
);
|
||||||
|
let rd_insert_clause = format!(
|
||||||
|
"INSERT INTO {rd_table} {rd_insert_columns} VALUES ($1, $2, $3, $4) RETURNING *"
|
||||||
|
);
|
||||||
|
let mut rd = sqlx::query_as::<_, ReleaseDataModel>(&rd_insert_clause)
|
||||||
|
.bind(name)
|
||||||
|
.bind(draft.data_type as i32)
|
||||||
|
.bind(draft.options.clone())
|
||||||
|
.bind(user_id.or(Some(draft.user_id)))
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
// 创建发布数据版本
|
||||||
|
let rdv = self
|
||||||
|
.insert_release_data_version(
|
||||||
|
CreateReleaseVersionData {
|
||||||
|
release_data_id: rd.id,
|
||||||
|
options: draft.options.clone(),
|
||||||
|
data: draft.data.unwrap(),
|
||||||
|
description: description.to_string(),
|
||||||
|
user_id: user_id.unwrap_or(draft.user_id),
|
||||||
|
},
|
||||||
|
&mut *tx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
// 更新发布数据使用的版本
|
// 更新发布数据使用的版本
|
||||||
let used_version_id = ReleaseDataColumn::UsedVersionId.name();
|
let used_version_id = ReleaseDataColumn::UsedVersionId.name();
|
||||||
let rd_id = ReleaseDataColumn::Id.name();
|
let rd_id = ReleaseDataColumn::Id.name();
|
||||||
@ -229,6 +312,7 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
&self,
|
&self,
|
||||||
draft_id: i32,
|
draft_id: i32,
|
||||||
description: &str,
|
description: &str,
|
||||||
|
user_id: Option<i32>,
|
||||||
) -> Result<(ReleaseDataModel, ReleaseDataVersionModel), DbAccessError> {
|
) -> Result<(ReleaseDataModel, ReleaseDataVersionModel), DbAccessError> {
|
||||||
// 查询草稿数据
|
// 查询草稿数据
|
||||||
let draft = self.query_draft_data_by_id(draft_id).await?;
|
let draft = self.query_draft_data_by_id(draft_id).await?;
|
||||||
@ -238,54 +322,52 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
"草稿未设置默认发布数据".to_string(),
|
"草稿未设置默认发布数据".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if draft.data.is_none() {
|
||||||
|
return Err(DbAccessError::DataError("草稿数据为空".to_string()));
|
||||||
|
}
|
||||||
// 查询默认发布数据
|
// 查询默认发布数据
|
||||||
let mut rd = self
|
let rd = self
|
||||||
.query_release_data_by_id(draft.default_release_data_id.unwrap())
|
.query_release_data_by_id(draft.default_release_data_id.unwrap())
|
||||||
.await?;
|
.await?;
|
||||||
// 开启事务
|
// 开启事务
|
||||||
let mut tx = self.pool.begin().await?;
|
let mut tx = self.pool.begin().await?;
|
||||||
// 创建发布数据版本
|
// 创建发布数据版本
|
||||||
let rdv_table = ReleaseDataVersionColumn::Table.name();
|
let rdv = self
|
||||||
let rdv_insert_columns = format!(
|
.insert_release_data_version(
|
||||||
"{}, {}, {}, {}",
|
CreateReleaseVersionData {
|
||||||
ReleaseDataVersionColumn::ReleaseDataId.name(),
|
release_data_id: rd.id,
|
||||||
ReleaseDataVersionColumn::Data.name(),
|
options: draft.options.clone(),
|
||||||
ReleaseDataVersionColumn::Description.name(),
|
data: draft.data.unwrap(),
|
||||||
ReleaseDataVersionColumn::UserId.name(),
|
description: description.to_string(),
|
||||||
);
|
user_id: user_id.unwrap_or(draft.user_id),
|
||||||
let rdv_insert_clause = format!(
|
},
|
||||||
"INSERT INTO {rdv_table} ({rdv_insert_columns}) VALUES ($1, $2, $3, $4) RETURNING *"
|
&mut *tx,
|
||||||
);
|
)
|
||||||
let rdv = sqlx::query_as::<_, ReleaseDataVersionModel>(&rdv_insert_clause)
|
|
||||||
.bind(rd.id)
|
|
||||||
.bind(draft.data)
|
|
||||||
.bind(description)
|
|
||||||
.bind(draft.user_id)
|
|
||||||
.fetch_one(&mut *tx)
|
|
||||||
.await?;
|
.await?;
|
||||||
// 更新发布数据使用的版本
|
// 更新发布数据使用的版本
|
||||||
let rd_table = ReleaseDataColumn::Table.name();
|
let rd_table = ReleaseDataColumn::Table.name();
|
||||||
let used_version_id = ReleaseDataColumn::UsedVersionId.name();
|
let used_version_id = ReleaseDataColumn::UsedVersionId.name();
|
||||||
|
let rd_options = ReleaseDataColumn::Options.name();
|
||||||
let rd_updated_at = ReleaseDataColumn::UpdatedAt.name();
|
let rd_updated_at = ReleaseDataColumn::UpdatedAt.name();
|
||||||
let rd_id = ReleaseDataColumn::Id.name();
|
let rd_id = ReleaseDataColumn::Id.name();
|
||||||
let rd_update_clause =
|
let rd_update_clause =
|
||||||
format!("UPDATE {rd_table} SET {used_version_id} = $1, {rd_updated_at} = 'now()' WHERE {rd_id} = $2");
|
format!("UPDATE {rd_table} SET {used_version_id} = $1, {rd_options} = $2, {rd_updated_at} = 'now()' WHERE {rd_id} = $3 RETURNING *");
|
||||||
sqlx::query(&rd_update_clause)
|
let rd = sqlx::query_as::<_, ReleaseDataModel>(&rd_update_clause)
|
||||||
.bind(rdv.id)
|
.bind(rdv.id)
|
||||||
|
.bind(draft.options.clone())
|
||||||
.bind(rd.id)
|
.bind(rd.id)
|
||||||
.execute(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
// 成功后提交事务
|
// 成功后提交事务
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
rd.used_version_id = Some(rdv.id);
|
|
||||||
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,
|
||||||
) -> 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}");
|
||||||
@ -295,7 +377,7 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
.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);
|
||||||
@ -305,19 +387,25 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
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(&self, name: &str) -> Result<bool, DbAccessError> {
|
async fn is_release_data_name_exist(
|
||||||
|
&self,
|
||||||
|
data_type: i32,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<bool, DbAccessError> {
|
||||||
let rd_table = ReleaseDataColumn::Table.name();
|
let rd_table = ReleaseDataColumn::Table.name();
|
||||||
let rd_name = ReleaseDataColumn::Name.name();
|
let rd_name = ReleaseDataColumn::Name.name();
|
||||||
|
let rd_data_type = ReleaseDataColumn::DataType.name();
|
||||||
let rd_query_clause = format!(
|
let rd_query_clause = format!(
|
||||||
"SELECT COUNT(*) FROM {rd_table} WHERE {rd_name} = $1",
|
"SELECT COUNT(*) FROM {rd_table} WHERE {rd_name} = $1 AND {rd_data_type} = $2",
|
||||||
rd_table = rd_table,
|
rd_table = rd_table,
|
||||||
rd_name = rd_name
|
rd_name = rd_name
|
||||||
);
|
);
|
||||||
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)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
@ -342,34 +430,66 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
Ok(rd)
|
Ok(rd)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn query_release_data_versions(
|
async fn query_release_data_names(
|
||||||
|
&self,
|
||||||
|
release_ids: &[i32],
|
||||||
|
) -> Result<Vec<(i32, String)>, DbAccessError> {
|
||||||
|
// 查询发布数据
|
||||||
|
let rd_table = ReleaseDataColumn::Table.name();
|
||||||
|
let rd_id = ReleaseDataColumn::Id.name();
|
||||||
|
let rd_name = ReleaseDataColumn::Name.name();
|
||||||
|
let select_columns = format!("{rd_id}, {rd_name}");
|
||||||
|
let rd_query_clause =
|
||||||
|
format!("SELECT {select_columns} FROM {rd_table} WHERE {rd_id} = ANY($1)",);
|
||||||
|
let rd = sqlx::query_as::<_, (i32, String)>(&rd_query_clause)
|
||||||
|
.bind(release_ids)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(rd)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn paging_query_release_data_version_list(
|
||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
) -> Result<Vec<ReleaseDataVersionModel>, DbAccessError> {
|
page: PageQuery,
|
||||||
|
) -> Result<PageData<ReleaseDataVersionModel>, DbAccessError> {
|
||||||
// 查询发布数据版本
|
// 查询发布数据版本
|
||||||
let rdv_table = ReleaseDataVersionColumn::Table.name();
|
let rdv_table = ReleaseDataVersionColumn::Table.name();
|
||||||
|
let rdv_release_id = ReleaseDataVersionColumn::ReleaseDataId.name();
|
||||||
|
// let where_clause = query.build_filter();
|
||||||
|
let count_clause = format!("SELECT COUNT(*) FROM {rdv_table} WHERE {rdv_release_id} = $1",);
|
||||||
|
|
||||||
|
// 查询总数
|
||||||
|
let total = sqlx::query_scalar(&count_clause)
|
||||||
|
.bind(release_id)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
if total == 0 {
|
||||||
|
return Ok(PageData::new(0, vec![]));
|
||||||
|
}
|
||||||
// 查询列,除了data列
|
// 查询列,除了data列
|
||||||
let rdv_columns = format!(
|
let rdv_columns = format!(
|
||||||
"{}, {}, {}, {}, {}, {}",
|
"{}, {}, {}, {}, {}, {}",
|
||||||
ReleaseDataVersionColumn::Id.name(),
|
ReleaseDataVersionColumn::Id.name(),
|
||||||
ReleaseDataVersionColumn::ReleaseDataId.name(),
|
ReleaseDataVersionColumn::ReleaseDataId.name(),
|
||||||
ReleaseDataVersionColumn::Version.name(),
|
ReleaseDataVersionColumn::Options.name(),
|
||||||
ReleaseDataVersionColumn::Description.name(),
|
ReleaseDataVersionColumn::Description.name(),
|
||||||
ReleaseDataVersionColumn::UserId.name(),
|
ReleaseDataVersionColumn::UserId.name(),
|
||||||
ReleaseDataVersionColumn::CreatedAt.name(),
|
ReleaseDataVersionColumn::CreatedAt.name(),
|
||||||
);
|
);
|
||||||
let rdv_release_id = ReleaseDataVersionColumn::ReleaseDataId.name();
|
let rdv_release_id = ReleaseDataVersionColumn::ReleaseDataId.name();
|
||||||
// 按版本号倒序排序
|
// 按版本号倒序排序
|
||||||
let sort = Sort::new(ReleaseDataVersionColumn::Version, SortOrder::Desc);
|
let sort = Sort::new(ReleaseDataVersionColumn::Id, SortOrder::Desc);
|
||||||
let order_by_clause = sort.to_order_by_clause();
|
let order_by_clause = sort.to_order_by_clause();
|
||||||
|
let paging_clause = page.to_limit_clause();
|
||||||
let rdv_query_clause = format!(
|
let rdv_query_clause = format!(
|
||||||
"SELECT {rdv_columns} FROM {rdv_table} WHERE {rdv_release_id} = $1 {order_by_clause}",
|
"SELECT {rdv_columns} FROM {rdv_table} WHERE {rdv_release_id} = $1 {order_by_clause} {paging_clause}",
|
||||||
);
|
);
|
||||||
let rdv = sqlx::query_as::<_, ReleaseDataVersionModel>(&rdv_query_clause)
|
let data = sqlx::query_as::<_, ReleaseDataVersionModel>(&rdv_query_clause)
|
||||||
.bind(release_id)
|
.bind(release_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(rdv)
|
Ok(PageData::new(total, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn query_release_data_version_by_id(
|
async fn query_release_data_version_by_id(
|
||||||
@ -391,6 +511,25 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
Ok(rdv)
|
Ok(rdv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn query_release_data_version_descriptions(
|
||||||
|
&self,
|
||||||
|
version_ids: &[i32],
|
||||||
|
) -> Result<Vec<(i32, String)>, DbAccessError> {
|
||||||
|
// 查询发布数据版本
|
||||||
|
let rdv_table = ReleaseDataVersionColumn::Table.name();
|
||||||
|
let rdv_id = ReleaseDataVersionColumn::Id.name();
|
||||||
|
let rdv_description = ReleaseDataVersionColumn::Description.name();
|
||||||
|
let select_columns = format!("{rdv_id}, {rdv_description}");
|
||||||
|
let rdv_query_clause =
|
||||||
|
format!("SELECT {select_columns} FROM {rdv_table} WHERE {rdv_id} = ANY($1)",);
|
||||||
|
let rdv = sqlx::query_as::<_, (i32, String)>(&rdv_query_clause)
|
||||||
|
.bind(version_ids)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(rdv)
|
||||||
|
}
|
||||||
|
|
||||||
async fn query_release_data_with_used_version(
|
async fn query_release_data_with_used_version(
|
||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
@ -430,7 +569,7 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
Ok(rd)
|
Ok(rd)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_release_data_published(
|
async fn set_release_data_published(
|
||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
is_published: bool,
|
is_published: bool,
|
||||||
@ -453,7 +592,7 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
Ok(rd)
|
Ok(rd)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_used_version(
|
async fn set_release_data_used_version(
|
||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
version_id: i32,
|
version_id: i32,
|
||||||
@ -479,7 +618,7 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
Ok(rd)
|
Ok(rd)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_draft_from_version(
|
async fn create_draft_from_release_version(
|
||||||
&self,
|
&self,
|
||||||
version_id: i32,
|
version_id: i32,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
@ -493,7 +632,8 @@ 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_data(&rdv.data)
|
.with_data(&rdv.data)
|
||||||
.with_default_release_data_id(rd.id),
|
.with_default_release_data_id(rd.id),
|
||||||
)
|
)
|
||||||
@ -504,12 +644,63 @@ impl ReleaseDataAccessor for RtssDbAccessor {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{CreateDraftData, DraftDataAccessor, RtssDbAccessor};
|
use crate::{CreateDraftData, DraftDataAccessor, RegisterUser, RtsaDbAccessor, UserAccessor};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use rtss_log::tracing::Level;
|
use rtsa_log::tracing::Level;
|
||||||
|
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() {
|
||||||
// 测试构造发布数据查询条件,名称过滤
|
// 测试构造发布数据查询条件,名称过滤
|
||||||
@ -524,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);
|
||||||
|
|
||||||
// 测试构造发布数据查询条件,是否上架过滤
|
// 测试构造发布数据查询条件,是否上架过滤
|
||||||
@ -538,7 +729,7 @@ 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);
|
||||||
}
|
}
|
||||||
@ -546,19 +737,33 @@ mod tests {
|
|||||||
// 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> {
|
||||||
rtss_log::Logging::default().with_level(Level::DEBUG).init();
|
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
||||||
let accessor = RtssDbAccessor::new(pool);
|
let accessor = RtsaDbAccessor::new(pool);
|
||||||
|
// 注册10个用户
|
||||||
|
for i in 0..10 {
|
||||||
|
let user = RegisterUser::new(&format!("test{}", i), "123456");
|
||||||
|
accessor.register_user(user).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", rtss_dto::common::DataType::Em, 1).with_data(data),
|
CreateDraftData::new("test", rtsa_dto::common::DataType::Iscs as i32, 1)
|
||||||
|
.with_options(
|
||||||
|
serde_json::to_value(IscsDataOptions {
|
||||||
|
style: IscsStyle::Style1,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.with_data(data),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let name = "test_release";
|
let name = "test_release";
|
||||||
let description = "test release";
|
let description = "test release";
|
||||||
// 发布到默认发布数据
|
// 发布到默认发布数据
|
||||||
let result = accessor.release_to_existing(draft.id, description).await;
|
let result = accessor
|
||||||
|
.release_to_existing(draft.id, description, None)
|
||||||
|
.await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
if let Some(e) = result.err() {
|
if let Some(e) = result.err() {
|
||||||
match e {
|
match e {
|
||||||
@ -571,31 +776,39 @@ mod tests {
|
|||||||
|
|
||||||
// 发布新版本
|
// 发布新版本
|
||||||
let (release_data, version1) = accessor
|
let (release_data, version1) = accessor
|
||||||
.release_new_from_draft(draft.id, name, &description)
|
.release_new_from_draft(draft.id, name, description, None)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(release_data.name, name);
|
assert_eq!(release_data.name, name);
|
||||||
// 检查使用版本是刚刚发布的版本
|
// 检查使用版本是刚刚发布的版本
|
||||||
assert_eq!(release_data.used_version_id, Some(version1.id));
|
assert_eq!(release_data.used_version_id, Some(version1.id));
|
||||||
|
// 检查options数据
|
||||||
|
let options: IscsDataOptions =
|
||||||
|
serde_json::from_value(release_data.options.unwrap()).unwrap();
|
||||||
|
assert_eq!(options.style, IscsStyle::Style1);
|
||||||
// 检查版本数据
|
// 检查版本数据
|
||||||
assert_eq!(version1.data, data);
|
assert_eq!(version1.data, data);
|
||||||
// 检查版本描述
|
// 检查版本描述
|
||||||
assert_eq!(version1.description, description);
|
assert_eq!(version1.description, description);
|
||||||
// 检查上架状态
|
// 检查上架状态
|
||||||
assert_eq!(release_data.is_published, true);
|
assert!(release_data.is_published);
|
||||||
// 检查草稿数据的默认发布数据id
|
// 检查草稿数据的默认发布数据id
|
||||||
let draft = accessor.query_draft_data_by_id(draft.id).await?;
|
let draft = accessor.query_draft_data_by_id(draft.id).await?;
|
||||||
assert_eq!(draft.default_release_data_id, Some(release_data.id));
|
assert_eq!(draft.default_release_data_id, Some(release_data.id));
|
||||||
println!("发布新版本测试成功");
|
println!("发布新版本测试成功");
|
||||||
|
|
||||||
// name重复检查
|
// name重复检查
|
||||||
let exist = accessor.is_release_data_name_exist(name).await?;
|
let exist = accessor
|
||||||
assert_eq!(exist, true);
|
.is_release_data_name_exist(DataType::Iscs as i32, name)
|
||||||
|
.await?;
|
||||||
|
assert!(exist);
|
||||||
|
|
||||||
// 修改草稿数据
|
// 修改草稿数据
|
||||||
let data = "test2".as_bytes();
|
let data = "test2".as_bytes();
|
||||||
accessor.update_draft_data_data(draft.id, data).await?;
|
accessor.update_draft_data_data(draft.id, data).await?;
|
||||||
// 发布到已存在发布数据成功测试
|
// 发布到已存在发布数据成功测试
|
||||||
let (release_data, version2) = accessor.release_to_existing(draft.id, description).await?;
|
let (release_data, version2) = accessor
|
||||||
|
.release_to_existing(draft.id, description, None)
|
||||||
|
.await?;
|
||||||
assert_eq!(release_data.name, name);
|
assert_eq!(release_data.name, name);
|
||||||
// 检查使用版本是刚刚发布的版本
|
// 检查使用版本是刚刚发布的版本
|
||||||
assert_eq!(release_data.used_version_id, Some(version2.id));
|
assert_eq!(release_data.used_version_id, Some(version2.id));
|
||||||
@ -614,15 +827,15 @@ mod tests {
|
|||||||
|
|
||||||
// 测试更新发布数据上架状态
|
// 测试更新发布数据上架状态
|
||||||
let release_data = accessor
|
let release_data = accessor
|
||||||
.update_release_data_published(release_data.id, false)
|
.set_release_data_published(release_data.id, false)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(release_data.is_published, false);
|
assert!(!release_data.is_published);
|
||||||
assert!(release_data.updated_at > release_data.created_at);
|
assert!(release_data.updated_at > release_data.created_at);
|
||||||
println!("更新发布数据上架状态测试成功");
|
println!("更新发布数据上架状态测试成功");
|
||||||
|
|
||||||
// 设置使用的版本
|
// 设置使用的版本
|
||||||
let release_data = accessor
|
let release_data = accessor
|
||||||
.set_used_version(release_data.id, version1.id)
|
.set_release_data_used_version(release_data.id, version1.id)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(release_data.used_version_id, Some(version1.id));
|
assert_eq!(release_data.used_version_id, Some(version1.id));
|
||||||
assert!(release_data.updated_at > release_data.created_at);
|
assert!(release_data.updated_at > release_data.created_at);
|
||||||
@ -630,9 +843,15 @@ mod tests {
|
|||||||
|
|
||||||
// 查询发布数据所有版本
|
// 查询发布数据所有版本
|
||||||
let versions = accessor
|
let versions = accessor
|
||||||
.query_release_data_versions(release_data.id)
|
.paging_query_release_data_version_list(
|
||||||
|
release_data.id,
|
||||||
|
PageQuery {
|
||||||
|
page: 1,
|
||||||
|
items_per_page: 10,
|
||||||
|
},
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(versions.len(), 2);
|
assert_eq!(versions.total, 2);
|
||||||
println!("查询发布数据所有版本测试成功: {:?}", versions);
|
println!("查询发布数据所有版本测试成功: {:?}", versions);
|
||||||
|
|
||||||
// 查询发布数据详情
|
// 查询发布数据详情
|
||||||
@ -644,11 +863,15 @@ mod tests {
|
|||||||
|
|
||||||
// 从版本创建草稿
|
// 从版本创建草稿
|
||||||
let draft = accessor
|
let draft = accessor
|
||||||
.create_draft_from_version(used_version.id, 1)
|
.create_draft_from_release_version(used_version.id, 1)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(draft.data, Some(used_version.data));
|
assert_eq!(draft.data, Some(used_version.data));
|
||||||
assert_eq!(draft.default_release_data_id, Some(release_data.id));
|
assert_eq!(draft.default_release_data_id, Some(release_data.id));
|
||||||
println!("从版本创建草稿测试成功");
|
assert_eq!(draft.options, used_version.options);
|
||||||
|
println!(
|
||||||
|
"从版本创建草稿测试成功: draft_data.options = {:?}",
|
||||||
|
draft.options
|
||||||
|
);
|
||||||
|
|
||||||
// 构造分页查询所需发布数据,4个人每人发布2个数据
|
// 构造分页查询所需发布数据,4个人每人发布2个数据
|
||||||
for i in 2..6 {
|
for i in 2..6 {
|
||||||
@ -657,46 +880,66 @@ 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, rtss_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
|
||||||
.release_new_from_draft(draft.id, &name, &description)
|
.release_new_from_draft(draft.id, &name, &description, None)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(release_data.name, name);
|
assert_eq!(release_data.name, name);
|
||||||
let another_name = format!("{}_another", name);
|
let another_name = format!("{}_another", name);
|
||||||
let (release_data, _) = accessor
|
let (release_data, _) = accessor
|
||||||
.release_new_from_draft(draft.id, &another_name, &description)
|
.release_new_from_draft(draft.id, &another_name, &description, None)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(release_data.name, another_name);
|
assert_eq!(release_data.name, another_name);
|
||||||
}
|
}
|
||||||
// 分页查询发布数据,无条件
|
// 分页查询发布数据,无条件
|
||||||
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(DataType::Em as i32);
|
||||||
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, 9);
|
assert_eq!(page_result.total, 8);
|
||||||
println!("分页查询发布数据测试成功");
|
println!("分页查询发布数据测试成功");
|
||||||
|
|
||||||
|
// 测试根据id列表查询发布数据name
|
||||||
|
let release_ids: Vec<i32> = page_result.data.iter().map(|d| d.id).collect();
|
||||||
|
let names = accessor
|
||||||
|
.query_release_data_names(release_ids.as_slice())
|
||||||
|
.await?;
|
||||||
|
println!("{:?}", names);
|
||||||
|
assert_eq!(names.len(), page_result.data.len());
|
||||||
|
|
||||||
|
// 测试根据数据版本id查询descriptions
|
||||||
|
let version_ids: Vec<i32> = page_result
|
||||||
|
.data
|
||||||
|
.iter()
|
||||||
|
.map(|d| d.used_version_id.unwrap())
|
||||||
|
.collect();
|
||||||
|
let description_map = accessor
|
||||||
|
.query_release_data_version_descriptions(version_ids.as_slice())
|
||||||
|
.await?;
|
||||||
|
println!("{:?}", description_map);
|
||||||
|
assert_eq!(description_map.len(), page_result.data.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
964
crates/rtsa_db/src/db_access/user.rs
Normal file
964
crates/rtsa_db/src/db_access/user.rs
Normal file
@ -0,0 +1,964 @@
|
|||||||
|
use serde_json::Value;
|
||||||
|
use sqlx::Postgres;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::{PageData, PageQuery, TableColumn},
|
||||||
|
model::{UserColumn, UserModel},
|
||||||
|
password_util::{self, verify_password},
|
||||||
|
username_util::{is_email, is_mobile},
|
||||||
|
DbAccessError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::RtsaDbAccessor;
|
||||||
|
|
||||||
|
/// 草稿数据管理
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait UserAccessor {
|
||||||
|
/// 用户注册
|
||||||
|
async fn register_user(&self, user: RegisterUser) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 用户更新信息
|
||||||
|
async fn update_user_info(&self, id: i32, info: Value) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 用户更新密码
|
||||||
|
async fn update_user_password(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
new_password: &str,
|
||||||
|
) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 更新用户手机
|
||||||
|
/// 警告:更新手机会检查是否已经存在,如果存在则返回错误
|
||||||
|
async fn update_user_mobile(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
new_mobile: &str,
|
||||||
|
) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 更新用户邮箱
|
||||||
|
/// 警告:更新邮箱会检查是否已经存在,如果存在则返回错误
|
||||||
|
async fn update_user_email(&self, id: i32, new_email: &str)
|
||||||
|
-> Result<UserModel, DbAccessError>;
|
||||||
|
/// 修改用户角色
|
||||||
|
async fn update_user_roles(&self, id: i32, roles: Value) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 用户登录查询
|
||||||
|
/// username: 用户名/邮箱/手机号
|
||||||
|
async fn query_user_login(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 查询用户数据
|
||||||
|
async fn query_user(&self, id: i32) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 根据username查询用户数据
|
||||||
|
async fn query_user_by_username(&self, username: &str) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 根据email查询用户数据
|
||||||
|
async fn query_user_by_email(&self, email: &str) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 根据mobile查询用户数据
|
||||||
|
async fn query_user_by_mobile(&self, mobile: &str) -> Result<UserModel, DbAccessError>;
|
||||||
|
/// 是否用户名已经存在
|
||||||
|
async fn is_user_name_exist(&self, name: &str) -> Result<bool, DbAccessError>;
|
||||||
|
/// 是否用户email已经存在
|
||||||
|
async fn is_user_email_exist(&self, email: &str) -> Result<bool, DbAccessError>;
|
||||||
|
/// 是否用户mobile已经存在
|
||||||
|
async fn is_user_mobile_exist(&self, mobile: &str) -> Result<bool, DbAccessError>;
|
||||||
|
/// 根据id列表查询用户name
|
||||||
|
async fn query_user_nicknames(&self, ids: &[i32]) -> Result<Vec<(i32, String)>, DbAccessError>;
|
||||||
|
/// 分页查询用户数据
|
||||||
|
async fn query_user_page(
|
||||||
|
&self,
|
||||||
|
page: PageQuery,
|
||||||
|
filter: UserPageFilter,
|
||||||
|
) -> Result<PageData<UserModel>, DbAccessError>;
|
||||||
|
/// 删除用户
|
||||||
|
/// 警告:删除用户会关联删除用户相关的所有数据
|
||||||
|
async fn delete_user(&self, id: i32) -> Result<(), DbAccessError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct UserPageFilter {
|
||||||
|
pub id: Option<i32>,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub nickname: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub mobile: Option<String>,
|
||||||
|
pub roles: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserPageFilter {
|
||||||
|
pub fn with_id(mut self, id: i32) -> Self {
|
||||||
|
self.id = Some(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_username(mut self, name: &str) -> Self {
|
||||||
|
self.username = Some(name.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_nickname(mut self, nickname: &str) -> Self {
|
||||||
|
self.nickname = Some(nickname.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_email(mut self, email: &str) -> Self {
|
||||||
|
self.email = Some(email.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_mobile(mut self, mobile: &str) -> Self {
|
||||||
|
self.mobile = Some(mobile.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_roles(mut self, roles: Value) -> Self {
|
||||||
|
self.roles = Some(roles);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_where_clause(&self) -> String {
|
||||||
|
let mut clauses = vec![];
|
||||||
|
let id_column = UserColumn::Id.name();
|
||||||
|
let name_column = UserColumn::Username.name();
|
||||||
|
let email_column = UserColumn::Email.name();
|
||||||
|
let mobile_column = UserColumn::Mobile.name();
|
||||||
|
let roles_column = UserColumn::Roles.name();
|
||||||
|
if let Some(id) = self.id {
|
||||||
|
clauses.push(format!(
|
||||||
|
"{id_column} = {id}",
|
||||||
|
id_column = id_column,
|
||||||
|
id = id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(name) = &self.username {
|
||||||
|
clauses.push(format!(
|
||||||
|
"{name_column} LIKE '%{name}%'",
|
||||||
|
name_column = name_column,
|
||||||
|
name = name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(nickname) = &self.nickname {
|
||||||
|
clauses.push(format!(
|
||||||
|
"{name_column} LIKE '%{nickname}%'",
|
||||||
|
name_column = name_column,
|
||||||
|
nickname = nickname
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(email) = &self.email {
|
||||||
|
clauses.push(format!(
|
||||||
|
"{email_column} LIKE '%{email}%'",
|
||||||
|
email_column = email_column,
|
||||||
|
email = email
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(mobile) = &self.mobile {
|
||||||
|
clauses.push(format!(
|
||||||
|
"{mobile_column} LIKE '%{mobile}%'",
|
||||||
|
mobile_column = mobile_column,
|
||||||
|
mobile = mobile
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(roles) = &self.roles {
|
||||||
|
clauses.push(format!(
|
||||||
|
"{roles_column} @> '{roles}'",
|
||||||
|
roles_column = roles_column,
|
||||||
|
roles = roles
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if clauses.is_empty() {
|
||||||
|
"".to_string()
|
||||||
|
} else {
|
||||||
|
format!("WHERE {}", clauses.join(" AND "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RegisterUser {
|
||||||
|
pub name: String,
|
||||||
|
pub nickname: String,
|
||||||
|
pub password: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub mobile: Option<String>,
|
||||||
|
pub roles: Value,
|
||||||
|
pub info: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegisterUser {
|
||||||
|
pub fn new(name: &str, password: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
nickname: name.to_string(),
|
||||||
|
password: password.to_string(),
|
||||||
|
email: None,
|
||||||
|
mobile: None,
|
||||||
|
roles: Value::Array(Default::default()),
|
||||||
|
info: Value::Object(Default::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_nickname(mut self, nickname: &str) -> Self {
|
||||||
|
self.nickname = nickname.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_email(mut self, email: &str) -> Self {
|
||||||
|
self.email = Some(email.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_mobile(mut self, mobile: &str) -> Self {
|
||||||
|
self.mobile = Some(mobile.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_roles(mut self, roles: Value) -> Self {
|
||||||
|
self.roles = roles;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_info(mut self, info: Value) -> Self {
|
||||||
|
self.info = info;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RtsaDbAccessor {
|
||||||
|
pub(crate) async fn insert_user<'e, 'c: 'e, E>(
|
||||||
|
&self,
|
||||||
|
user: RegisterUser,
|
||||||
|
executor: E,
|
||||||
|
) -> Result<UserModel, DbAccessError>
|
||||||
|
where
|
||||||
|
E: 'e + sqlx::Executor<'c, Database = Postgres>,
|
||||||
|
{
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let username = UserColumn::Username.name();
|
||||||
|
let password = UserColumn::Password.name();
|
||||||
|
let nickname = UserColumn::Nickname.name();
|
||||||
|
let email = UserColumn::Email.name();
|
||||||
|
let mobile = UserColumn::Mobile.name();
|
||||||
|
let roles = UserColumn::Roles.name();
|
||||||
|
let info = UserColumn::Info.name();
|
||||||
|
let insert_clause = format!(
|
||||||
|
"INSERT INTO {table} ({username}, {password}, {nickname}, {email}, {mobile}, {roles}, {info})
|
||||||
|
VALUES ('{new_username}', '{new_password}', '{new_nickname}', {new_email}, {new_mobile}, '{new_roles}', '{new_info}')
|
||||||
|
RETURNING *",
|
||||||
|
table = table,
|
||||||
|
username = username,
|
||||||
|
new_username = user.name,
|
||||||
|
password = password,
|
||||||
|
new_password = user.password,
|
||||||
|
nickname = nickname,
|
||||||
|
new_nickname = user.nickname,
|
||||||
|
email = email,
|
||||||
|
new_email = user.email.as_deref().map(|s| format!("'{s}'")).unwrap_or("NULL".to_string()),
|
||||||
|
mobile = mobile,
|
||||||
|
new_mobile = user.mobile.as_deref().map(|s| format!("'{s}'")).unwrap_or("NULL".to_string()),
|
||||||
|
roles = roles,
|
||||||
|
new_roles = user.roles,
|
||||||
|
info = info,
|
||||||
|
new_info = user.info,
|
||||||
|
);
|
||||||
|
let user: UserModel = sqlx::query_as(&insert_clause).fetch_one(executor).await?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserAccessor for RtsaDbAccessor {
|
||||||
|
async fn query_user_nicknames(&self, ids: &[i32]) -> Result<Vec<(i32, String)>, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let id_col = UserColumn::Id.name();
|
||||||
|
let nickname_col = UserColumn::Nickname.name();
|
||||||
|
let select_columns = format!("{id_col}, {nickname_col}",);
|
||||||
|
let query_clause =
|
||||||
|
format!("SELECT {select_columns} FROM {table} WHERE {id_col} = ANY($1)",);
|
||||||
|
let rows = sqlx::query_as::<_, (i32, String)>(&query_clause)
|
||||||
|
.bind(ids)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_user_page(
|
||||||
|
&self,
|
||||||
|
page: PageQuery,
|
||||||
|
filter: UserPageFilter,
|
||||||
|
) -> Result<PageData<UserModel>, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let id_column = UserColumn::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 query_clause =
|
||||||
|
format!("SELECT * FROM {table} {where_clause} ORDER BY {id_column} {limit_clause}",);
|
||||||
|
let rows = sqlx::query_as::<_, UserModel>(&query_clause)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(PageData::new(total, rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register_user(&self, mut user: RegisterUser) -> Result<UserModel, DbAccessError> {
|
||||||
|
user.password = password_util::hash_password(&user.password)?;
|
||||||
|
self.insert_user(user, &self.pool).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_user_login(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<UserModel, DbAccessError> {
|
||||||
|
let user: UserModel;
|
||||||
|
if is_email(username) {
|
||||||
|
let query_result = self.query_user_by_email(username).await;
|
||||||
|
if query_result.is_ok() {
|
||||||
|
user = query_result.unwrap();
|
||||||
|
} else {
|
||||||
|
return Err(DbAccessError::UserNotExist("email".to_string()));
|
||||||
|
}
|
||||||
|
} else if is_mobile(username) {
|
||||||
|
let query_result = self.query_user_by_mobile(username).await;
|
||||||
|
if query_result.is_ok() {
|
||||||
|
user = query_result.unwrap();
|
||||||
|
} else {
|
||||||
|
return Err(DbAccessError::UserNotExist("mobile".to_string()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let query_result = self.query_user_by_username(username).await;
|
||||||
|
if query_result.is_ok() {
|
||||||
|
user = query_result.unwrap();
|
||||||
|
} else {
|
||||||
|
return Err(DbAccessError::UserNotExist("username".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if verify_password(password, &user.password).is_ok() {
|
||||||
|
Ok(user)
|
||||||
|
} else {
|
||||||
|
Err(DbAccessError::PasswordNotMatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_user(&self, id: i32) -> Result<UserModel, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let id_column = UserColumn::Id.name();
|
||||||
|
let query_clause = format!("SELECT * FROM {table} WHERE {id_column} = {id}",);
|
||||||
|
let user = sqlx::query_as(&query_clause)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
match user {
|
||||||
|
None => Err(DbAccessError::RowNotExist(
|
||||||
|
UserColumn::Table.name().to_string(),
|
||||||
|
)),
|
||||||
|
Some(user) => Ok(user),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_user_by_username(&self, username: &str) -> Result<UserModel, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let username_column = UserColumn::Username.name();
|
||||||
|
let query_clause = format!(
|
||||||
|
"SELECT * FROM {table} WHERE {username_column} = $1 LIMIT 1",
|
||||||
|
table = table,
|
||||||
|
username_column = username_column,
|
||||||
|
);
|
||||||
|
let user = sqlx::query_as(&query_clause)
|
||||||
|
.bind(username)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
match user {
|
||||||
|
None => Err(DbAccessError::RowNotExist(
|
||||||
|
UserColumn::Table.name().to_string(),
|
||||||
|
)),
|
||||||
|
Some(user) => Ok(user),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_user_by_email(&self, email: &str) -> Result<UserModel, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let email_column = UserColumn::Email.name();
|
||||||
|
let query_clause = format!(
|
||||||
|
"SELECT * FROM {table} WHERE {email_column} = $1 LIMIT 1",
|
||||||
|
table = table,
|
||||||
|
email_column = email_column,
|
||||||
|
);
|
||||||
|
let user = sqlx::query_as(&query_clause)
|
||||||
|
.bind(email)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
match user {
|
||||||
|
None => Err(DbAccessError::RowNotExist(
|
||||||
|
UserColumn::Table.name().to_string(),
|
||||||
|
)),
|
||||||
|
Some(user) => Ok(user),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_user_by_mobile(&self, mobile: &str) -> Result<UserModel, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let mobile_column = UserColumn::Mobile.name();
|
||||||
|
let query_clause = format!(
|
||||||
|
"SELECT * FROM {table} WHERE {mobile_column} = $1 LIMIT 1",
|
||||||
|
table = table,
|
||||||
|
mobile_column = mobile_column,
|
||||||
|
);
|
||||||
|
let user = sqlx::query_as(&query_clause)
|
||||||
|
.bind(mobile)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
match user {
|
||||||
|
None => Err(DbAccessError::RowNotExist(
|
||||||
|
UserColumn::Table.name().to_string(),
|
||||||
|
)),
|
||||||
|
Some(user) => Ok(user),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_user_info(&self, id: i32, info: Value) -> Result<UserModel, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let id_column = UserColumn::Id.name();
|
||||||
|
let info_column = UserColumn::Info.name();
|
||||||
|
let updated_at_column = UserColumn::UpdatedAt.name();
|
||||||
|
if info.is_null() {
|
||||||
|
Err(DbAccessError::InvalidArgument("info is null".to_string()))
|
||||||
|
} else {
|
||||||
|
let update_clause = format!(
|
||||||
|
"UPDATE {table} SET {info_column} = '{new_info}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *",
|
||||||
|
table = table,
|
||||||
|
info_column = info_column,
|
||||||
|
new_info = info,
|
||||||
|
id_column = id_column,
|
||||||
|
id = id
|
||||||
|
);
|
||||||
|
let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_user_password(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
new_password: &str,
|
||||||
|
) -> Result<UserModel, DbAccessError> {
|
||||||
|
if new_password.is_empty() || new_password.len() < 6 {
|
||||||
|
return Err(DbAccessError::InvalidArgument(
|
||||||
|
"new_password is empty or too short".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if new_password.len() > 64 {
|
||||||
|
return Err(DbAccessError::InvalidArgument(
|
||||||
|
"new_password is too long".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let hashed = password_util::hash_password(new_password)?;
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let id_column = UserColumn::Id.name();
|
||||||
|
let password_column = UserColumn::Password.name();
|
||||||
|
let updated_at_column = UserColumn::UpdatedAt.name();
|
||||||
|
let update_clause = format!(
|
||||||
|
"UPDATE {table} SET {password_column} = '{hashed}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *",
|
||||||
|
table = table,
|
||||||
|
password_column = password_column,
|
||||||
|
hashed = hashed,
|
||||||
|
id_column = id_column,
|
||||||
|
id = id
|
||||||
|
);
|
||||||
|
let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_user_mobile(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
new_mobile: &str,
|
||||||
|
) -> Result<UserModel, DbAccessError> {
|
||||||
|
if new_mobile.is_empty() {
|
||||||
|
return Err(DbAccessError::InvalidArgument(
|
||||||
|
"mobile is empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.is_user_mobile_exist(new_mobile).await? {
|
||||||
|
return Err(DbAccessError::InvalidArgument(
|
||||||
|
"mobile already exists".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let id_column = UserColumn::Id.name();
|
||||||
|
let mobile_column = UserColumn::Mobile.name();
|
||||||
|
let updated_at_column = UserColumn::UpdatedAt.name();
|
||||||
|
let update_clause = format!(
|
||||||
|
"UPDATE {table} SET {mobile_column} = '{new_mobile}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *",
|
||||||
|
table = table,
|
||||||
|
mobile_column = mobile_column,
|
||||||
|
new_mobile = new_mobile,
|
||||||
|
id_column = id_column,
|
||||||
|
id = id
|
||||||
|
);
|
||||||
|
let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_user_email(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
new_email: &str,
|
||||||
|
) -> Result<UserModel, DbAccessError> {
|
||||||
|
if new_email.is_empty() || !new_email.contains('@') {
|
||||||
|
return Err(DbAccessError::InvalidArgument(
|
||||||
|
"email is invalid".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.is_user_email_exist(new_email).await? {
|
||||||
|
return Err(DbAccessError::InvalidArgument(
|
||||||
|
"email already exists".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let id_column = UserColumn::Id.name();
|
||||||
|
let email_column = UserColumn::Email.name();
|
||||||
|
let updated_at_column = UserColumn::UpdatedAt.name();
|
||||||
|
let update_clause = format!(
|
||||||
|
"UPDATE {table} SET {email_column} = '{new_email}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *",
|
||||||
|
table = table,
|
||||||
|
email_column = email_column,
|
||||||
|
new_email = new_email,
|
||||||
|
id_column = id_column,
|
||||||
|
id = id
|
||||||
|
);
|
||||||
|
let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_user_roles(&self, id: i32, roles: Value) -> Result<UserModel, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let id_column = UserColumn::Id.name();
|
||||||
|
let roles_column = UserColumn::Roles.name();
|
||||||
|
if roles.is_null() {
|
||||||
|
return Err(DbAccessError::InvalidArgument("roles is null".to_string()));
|
||||||
|
}
|
||||||
|
let updated_at_column = UserColumn::UpdatedAt.name();
|
||||||
|
let update_clause = format!(
|
||||||
|
"UPDATE {table} SET {roles_column} = '{new_roles}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *",
|
||||||
|
table = table,
|
||||||
|
roles_column = roles_column,
|
||||||
|
new_roles = serde_json::to_value(roles).unwrap(),
|
||||||
|
id_column = id_column,
|
||||||
|
id = id
|
||||||
|
);
|
||||||
|
let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_user_name_exist(&self, name: &str) -> Result<bool, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let username_column = UserColumn::Username.name();
|
||||||
|
let query_clause = format!(
|
||||||
|
"SELECT COUNT(*) FROM {table} WHERE {username_column} = '{name}'",
|
||||||
|
table = table,
|
||||||
|
username_column = username_column,
|
||||||
|
name = name
|
||||||
|
);
|
||||||
|
let count: i64 = sqlx::query_scalar(&query_clause)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(count > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_user_email_exist(&self, email: &str) -> Result<bool, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let email_column = UserColumn::Email.name();
|
||||||
|
let query_clause = format!(
|
||||||
|
"SELECT COUNT(*) FROM {table} WHERE {email_column} = '{email}'",
|
||||||
|
table = table,
|
||||||
|
email_column = email_column,
|
||||||
|
email = email
|
||||||
|
);
|
||||||
|
let count: i64 = sqlx::query_scalar(&query_clause)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(count > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_user_mobile_exist(&self, mobile: &str) -> Result<bool, DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let mobile_column = UserColumn::Mobile.name();
|
||||||
|
let query_clause = format!(
|
||||||
|
"SELECT COUNT(*) FROM {table} WHERE {mobile_column} = '{mobile}'",
|
||||||
|
table = table,
|
||||||
|
mobile_column = mobile_column,
|
||||||
|
mobile = mobile
|
||||||
|
);
|
||||||
|
let count: i64 = sqlx::query_scalar(&query_clause)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(count > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_user(&self, id: i32) -> Result<(), DbAccessError> {
|
||||||
|
let table = UserColumn::Table.name();
|
||||||
|
let id_column = UserColumn::Id.name();
|
||||||
|
let delete_clause = format!("DELETE FROM {table} WHERE {id_column} = {id}",);
|
||||||
|
sqlx::query(&delete_clause)
|
||||||
|
.bind(id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rtsa_dto::common::Role;
|
||||||
|
use rtsa_log::tracing::info;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_register_user(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
rtsa_log::Logging::default().init();
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password")
|
||||||
|
.with_roles(serde_json::to_value(vec![Role::User]).unwrap())
|
||||||
|
.with_email("test_user@example.com")
|
||||||
|
.with_mobile("13345678901")
|
||||||
|
.with_info(serde_json::json!({"from": "cgy"}));
|
||||||
|
accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
let queried_user = accessor
|
||||||
|
.query_user_login("test_user@example.com", "test_password")
|
||||||
|
.await?;
|
||||||
|
info!("queried_user: {:?}", queried_user);
|
||||||
|
assert_eq!(queried_user.username, "test_user");
|
||||||
|
assert_eq!(
|
||||||
|
queried_user.email,
|
||||||
|
Some("test_user@example.com".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(queried_user.mobile, Some("13345678901".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
queried_user.roles,
|
||||||
|
serde_json::to_value(vec![Role::User]).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(queried_user.info, serde_json::json!({"from": "cgy"}));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_update_user_info(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// Register a new user
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password")
|
||||||
|
.with_info(serde_json::json!({"from": "cgy"}));
|
||||||
|
let user = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Update user info
|
||||||
|
let updated_user = accessor
|
||||||
|
.update_user_info(user.id, serde_json::json!({"from": "updated"}))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Verify the update
|
||||||
|
assert_eq!(updated_user.info, serde_json::json!({"from": "updated"}));
|
||||||
|
|
||||||
|
let updated_user = accessor.update_user_info(user.id, Value::Null).await;
|
||||||
|
assert!(updated_user.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_update_user_password(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// Register a new user
|
||||||
|
let new_user =
|
||||||
|
RegisterUser::new("test_user", "old_password").with_email("test_user@example.com");
|
||||||
|
let user = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Update user password with empty password
|
||||||
|
let result = accessor.update_user_password(user.id, "").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// Update user password with short password
|
||||||
|
let result = accessor.update_user_password(user.id, "short").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// Update user password
|
||||||
|
let _ = accessor
|
||||||
|
.update_user_password(user.id, "new_password")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Verify the password has changed by attempting to login
|
||||||
|
let result = accessor
|
||||||
|
.query_user_login("test_user@example.com", "new_password")
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let result = accessor
|
||||||
|
.query_user_login("test_user@example.com", "old_password")
|
||||||
|
.await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_update_user_mobile(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// Register a new user with mobile number
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password").with_mobile("13345678901");
|
||||||
|
let user = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Update user mobile
|
||||||
|
let updated_user = accessor.update_user_mobile(user.id, "0987654321").await?;
|
||||||
|
|
||||||
|
// Verify the update
|
||||||
|
assert_eq!(updated_user.mobile, Some("0987654321".to_string()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_update_user_email(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// Register a new user with email
|
||||||
|
let new_user =
|
||||||
|
RegisterUser::new("test_user", "test_password").with_email("test_user@example.com");
|
||||||
|
let user = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Update user email
|
||||||
|
let updated_user = accessor
|
||||||
|
.update_user_email(user.id, "newemail@example.com")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Verify the update
|
||||||
|
assert_eq!(updated_user.email, Some("newemail@example.com".to_string()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_update_user_roles(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// Register a new user with default role
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password");
|
||||||
|
let user = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Update user roles
|
||||||
|
let new_roles = vec![Role::Admin, Role::User];
|
||||||
|
let updated_user = accessor
|
||||||
|
.update_user_roles(user.id, serde_json::to_value(new_roles.clone()).unwrap())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Verify the update
|
||||||
|
assert_eq!(updated_user.roles, serde_json::to_value(new_roles).unwrap());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_query_user(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password");
|
||||||
|
let um = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Assume a user with ID exists
|
||||||
|
let user = accessor.query_user(um.id).await?;
|
||||||
|
assert_eq!(user.id, um.id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_query_user_by_username(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password");
|
||||||
|
let um = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Assume a user with username exists
|
||||||
|
let user = accessor.query_user_by_username("test_user").await?;
|
||||||
|
assert_eq!(user.id, um.id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_query_user_by_email(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password").with_email("test@test.cn");
|
||||||
|
let um = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Assume a user with email exists
|
||||||
|
let user = accessor.query_user_by_email("test@test.cn").await?;
|
||||||
|
assert_eq!(user.id, um.id);
|
||||||
|
|
||||||
|
// Assume a user with email does not exist
|
||||||
|
let result = accessor.query_user_by_email("dev@test.cn").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_query_user_by_mobile(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password").with_mobile("13345678901");
|
||||||
|
let um = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Assume a user with mobile exists
|
||||||
|
let user = accessor.query_user_by_mobile("13345678901").await?;
|
||||||
|
assert_eq!(user.id, um.id);
|
||||||
|
|
||||||
|
// Assume a user with mobile does not exist
|
||||||
|
let result = accessor.query_user_by_mobile("13345678902").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_query_user_login(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password")
|
||||||
|
.with_email("test_user@example.com")
|
||||||
|
.with_mobile("13345678901");
|
||||||
|
accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Test with correct credentials
|
||||||
|
let user = accessor
|
||||||
|
.query_user_login("test_user@example.com", "test_password")
|
||||||
|
.await?;
|
||||||
|
assert_eq!(user.username, "test_user");
|
||||||
|
|
||||||
|
let user = accessor
|
||||||
|
.query_user_login("test_user", "test_password")
|
||||||
|
.await;
|
||||||
|
assert!(user.is_ok());
|
||||||
|
|
||||||
|
let user = accessor
|
||||||
|
.query_user_login("13345678901", "test_password")
|
||||||
|
.await;
|
||||||
|
assert!(user.is_ok());
|
||||||
|
|
||||||
|
// Test with incorrect credentials
|
||||||
|
let result = accessor
|
||||||
|
.query_user_login("test_user@example.com", "wrong_password")
|
||||||
|
.await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_query_user_name(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
// 初始化数据
|
||||||
|
for i in 0..3 {
|
||||||
|
let new_user = RegisterUser::new(&format!("test_user{}", i), "test_password");
|
||||||
|
let model = accessor.register_user(new_user).await?;
|
||||||
|
ids.push(model.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let names = accessor.query_user_nicknames(&ids).await?;
|
||||||
|
assert_eq!(names.len(), ids.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_query_user_page(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// 初始化测试数据
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password");
|
||||||
|
accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
let page_query = PageQuery::new(1, 10);
|
||||||
|
let filter = UserPageFilter::default().with_username("test");
|
||||||
|
let page_result = accessor.query_user_page(page_query, filter).await?;
|
||||||
|
assert!(page_result.total >= 0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_is_user_email_exist(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// Register a new user with email
|
||||||
|
let new_user =
|
||||||
|
RegisterUser::new("test_user", "test_password").with_email("user@example.com");
|
||||||
|
accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Test with existing email
|
||||||
|
let result = accessor.is_user_email_exist("user@example.com").await?;
|
||||||
|
assert!(result);
|
||||||
|
|
||||||
|
// Test with non-existing email
|
||||||
|
let result = accessor
|
||||||
|
.is_user_email_exist("non-exist@example.com")
|
||||||
|
.await?;
|
||||||
|
assert!(!result);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_is_user_mobile_exist(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// Register a new user with mobile
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password").with_mobile("13345678901");
|
||||||
|
accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Test with existing mobile
|
||||||
|
let result = accessor.is_user_mobile_exist("13345678901").await?;
|
||||||
|
assert!(result);
|
||||||
|
|
||||||
|
// Test with non-existing mobile
|
||||||
|
let result = accessor.is_user_mobile_exist("0987654321").await?;
|
||||||
|
assert!(!result);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_is_user_name_exist(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// Register a new user with name
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password");
|
||||||
|
accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Test with existing name
|
||||||
|
let result = accessor.is_user_name_exist("test_user").await?;
|
||||||
|
assert!(result);
|
||||||
|
|
||||||
|
// Test with non-existing name
|
||||||
|
let result = accessor.is_user_name_exist("non-exist").await?;
|
||||||
|
assert!(!result);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
|
async fn test_delete_user(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
|
let accessor = RtsaDbAccessor { pool };
|
||||||
|
|
||||||
|
// Register a new user
|
||||||
|
let new_user = RegisterUser::new("test_user", "test_password");
|
||||||
|
let user = accessor.register_user(new_user).await?;
|
||||||
|
|
||||||
|
// Delete the user
|
||||||
|
accessor.delete_user(user.id).await?;
|
||||||
|
|
||||||
|
// Verify the user has been deleted
|
||||||
|
let result = accessor.query_user(user.id).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
21
crates/rtsa_db/src/error.rs
Normal file
21
crates/rtsa_db/src/error.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum DbAccessError {
|
||||||
|
#[error("未知的数据库访问错误")]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
#[error("数据访问错误: {0}")]
|
||||||
|
SqlxError(#[from] sqlx::Error),
|
||||||
|
#[error("数据已存在: {0}")]
|
||||||
|
RowExist(String),
|
||||||
|
#[error("数据不存在: {0}")]
|
||||||
|
RowNotExist(String),
|
||||||
|
#[error("数据错误:{0}")]
|
||||||
|
DataError(String),
|
||||||
|
#[error("非法参数:{0}")]
|
||||||
|
InvalidArgument(String),
|
||||||
|
#[error("用户不存在:{0}")]
|
||||||
|
UserNotExist(String),
|
||||||
|
#[error("密码不正确")]
|
||||||
|
PasswordNotMatch,
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
571
crates/rtsa_db/src/model.rs
Normal file
571
crates/rtsa_db/src/model.rs
Normal file
@ -0,0 +1,571 @@
|
|||||||
|
use serde_json::Value;
|
||||||
|
use sqlx::types::chrono::{DateTime, Local};
|
||||||
|
|
||||||
|
use crate::common::TableColumn;
|
||||||
|
|
||||||
|
pub enum MqttClientIdSeq {
|
||||||
|
Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttClientIdSeq {
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
MqttClientIdSeq::Name => "rtsa.mqtt_client_id_seq",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum UserColumn {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Username,
|
||||||
|
Password,
|
||||||
|
Nickname,
|
||||||
|
Email,
|
||||||
|
Mobile,
|
||||||
|
Roles,
|
||||||
|
Info,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct UserModel {
|
||||||
|
pub id: i32,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
pub nickname: String,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub email: Option<String>,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub mobile: Option<String>,
|
||||||
|
pub roles: Value,
|
||||||
|
pub info: Value,
|
||||||
|
pub created_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 列映射
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DraftDataColumn {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
DataType,
|
||||||
|
Options,
|
||||||
|
Data,
|
||||||
|
DefaultReleaseDataId,
|
||||||
|
UserId,
|
||||||
|
IsShared,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct DraftDataModel {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub data_type: i32,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub options: Option<Value>,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub data: Option<Vec<u8>>,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub default_release_data_id: Option<i32>,
|
||||||
|
pub user_id: i32,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub is_shared: bool,
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
|
pub updated_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 rtsa.release_data 列映射
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ReleaseDataColumn {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
DataType,
|
||||||
|
Options,
|
||||||
|
UsedVersionId,
|
||||||
|
UserId,
|
||||||
|
IsPublished,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct ReleaseDataModel {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub data_type: i32,
|
||||||
|
/// 从发布版本复制的选项,主要用于查询
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub options: Option<Value>,
|
||||||
|
pub used_version_id: Option<i32>,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub is_published: bool,
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
|
pub updated_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 rtsa.release_data_version 列映射
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ReleaseDataVersionColumn {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
ReleaseDataId,
|
||||||
|
Options,
|
||||||
|
Data,
|
||||||
|
Description,
|
||||||
|
UserId,
|
||||||
|
CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct ReleaseDataVersionModel {
|
||||||
|
pub id: i32,
|
||||||
|
pub release_data_id: i32,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub options: Option<Value>,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub description: String,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 rtsa.release_data_set 列映射
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ReleaseDataSetColumn {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
DataSetType,
|
||||||
|
Name,
|
||||||
|
Description,
|
||||||
|
Config,
|
||||||
|
IsPublished,
|
||||||
|
CreatorId,
|
||||||
|
CreatedAt,
|
||||||
|
UpdaterId,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct ReleaseDataSetModel {
|
||||||
|
pub id: i32,
|
||||||
|
pub data_set_type: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub config: Value,
|
||||||
|
pub is_published: bool,
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
|
pub updater_id: i32,
|
||||||
|
pub updated_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 rtsa.release_data_set_data 列映射
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ReleaseDataSetDataColumn {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
ReleaseDataSetId,
|
||||||
|
ReleaseDataId,
|
||||||
|
OrderIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct ReleaseDataSetDataModel {
|
||||||
|
pub id: i32,
|
||||||
|
pub release_data_set_id: i32,
|
||||||
|
pub release_data_id: i32,
|
||||||
|
pub order_index: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 rtsa.user_operation_log 列映射
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum UserOperationLogColumn {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
UserId,
|
||||||
|
LogType,
|
||||||
|
LogData,
|
||||||
|
CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct UserOperationLogModel {
|
||||||
|
pub id: i32,
|
||||||
|
pub organization_id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub log_type: i32,
|
||||||
|
pub log_data: Value,
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for UserColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
UserColumn::Table => "rtsa.user",
|
||||||
|
UserColumn::Id => "id",
|
||||||
|
UserColumn::Username => "username",
|
||||||
|
UserColumn::Password => "password",
|
||||||
|
UserColumn::Nickname => "nickname",
|
||||||
|
UserColumn::Email => "email",
|
||||||
|
UserColumn::Mobile => "mobile",
|
||||||
|
UserColumn::Roles => "roles",
|
||||||
|
UserColumn::Info => "info",
|
||||||
|
UserColumn::CreatedAt => "created_at",
|
||||||
|
UserColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for UserDataColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
UserDataColumn::Table => "rtsa.user_data",
|
||||||
|
UserDataColumn::Id => "id",
|
||||||
|
UserDataColumn::UserId => "user_id",
|
||||||
|
UserDataColumn::DataType => "data_type",
|
||||||
|
UserDataColumn::Data => "data",
|
||||||
|
UserDataColumn::CreatedAt => "created_at",
|
||||||
|
UserDataColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for OrganizationColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
OrganizationColumn::Table => "rtsa.organization",
|
||||||
|
OrganizationColumn::Id => "id",
|
||||||
|
OrganizationColumn::Code => "code",
|
||||||
|
OrganizationColumn::Name => "name",
|
||||||
|
OrganizationColumn::Config => "config",
|
||||||
|
OrganizationColumn::ParentId => "parent_id",
|
||||||
|
OrganizationColumn::CreatorId => "creator_id",
|
||||||
|
OrganizationColumn::CreatedAt => "created_at",
|
||||||
|
OrganizationColumn::UpdaterId => "updater_id",
|
||||||
|
OrganizationColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for OrganizationUserColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
OrganizationUserColumn::Table => "rtsa.organization_user",
|
||||||
|
OrganizationUserColumn::Id => "id",
|
||||||
|
OrganizationUserColumn::OrganizationId => "organization_id",
|
||||||
|
OrganizationUserColumn::UserId => "user_id",
|
||||||
|
OrganizationUserColumn::StudentId => "student_id",
|
||||||
|
OrganizationUserColumn::Roles => "roles",
|
||||||
|
OrganizationUserColumn::Info => "info",
|
||||||
|
OrganizationUserColumn::CreatorId => "creator_id",
|
||||||
|
OrganizationUserColumn::CreatedAt => "created_at",
|
||||||
|
OrganizationUserColumn::UpdaterId => "updater_id",
|
||||||
|
OrganizationUserColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for OrganizationDataColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
OrganizationDataColumn::Table => "rtsa.organization_data",
|
||||||
|
OrganizationDataColumn::Id => "id",
|
||||||
|
OrganizationDataColumn::OrganizationId => "organization_id",
|
||||||
|
OrganizationDataColumn::DataType => "data_type",
|
||||||
|
OrganizationDataColumn::Data => "data",
|
||||||
|
OrganizationDataColumn::CreatorId => "creator_id",
|
||||||
|
OrganizationDataColumn::CreatedAt => "created_at",
|
||||||
|
OrganizationDataColumn::UpdaterId => "updater_id",
|
||||||
|
OrganizationDataColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for FeatureColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
FeatureColumn::Table => "rtsa.feature",
|
||||||
|
FeatureColumn::Id => "id",
|
||||||
|
FeatureColumn::FeatureType => "feature_type",
|
||||||
|
FeatureColumn::Name => "name",
|
||||||
|
FeatureColumn::Description => "description",
|
||||||
|
FeatureColumn::Config => "config",
|
||||||
|
FeatureColumn::IsPublished => "is_published",
|
||||||
|
FeatureColumn::CreatorId => "creator_id",
|
||||||
|
FeatureColumn::UpdaterId => "updater_id",
|
||||||
|
FeatureColumn::CreatedAt => "created_at",
|
||||||
|
FeatureColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for OrganizationFeatureColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
OrganizationFeatureColumn::Table => "rtsa.organization_feature",
|
||||||
|
OrganizationFeatureColumn::Id => "id",
|
||||||
|
OrganizationFeatureColumn::OrganizationId => "organization_id",
|
||||||
|
OrganizationFeatureColumn::FeatureId => "feature_id",
|
||||||
|
OrganizationFeatureColumn::Config => "config",
|
||||||
|
OrganizationFeatureColumn::CreatorId => "creator_id",
|
||||||
|
OrganizationFeatureColumn::CreatedAt => "created_at",
|
||||||
|
OrganizationFeatureColumn::UpdaterId => "updater_id",
|
||||||
|
OrganizationFeatureColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for DraftDataColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
DraftDataColumn::Table => "rtsa.draft_data",
|
||||||
|
DraftDataColumn::Id => "id",
|
||||||
|
DraftDataColumn::Name => "name",
|
||||||
|
DraftDataColumn::DataType => "data_type",
|
||||||
|
DraftDataColumn::Options => "options",
|
||||||
|
DraftDataColumn::Data => "data",
|
||||||
|
DraftDataColumn::DefaultReleaseDataId => "default_release_data_id",
|
||||||
|
DraftDataColumn::UserId => "user_id",
|
||||||
|
DraftDataColumn::IsShared => "is_shared",
|
||||||
|
DraftDataColumn::CreatedAt => "created_at",
|
||||||
|
DraftDataColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for ReleaseDataColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ReleaseDataColumn::Table => "rtsa.release_data",
|
||||||
|
ReleaseDataColumn::Id => "id",
|
||||||
|
ReleaseDataColumn::Name => "name",
|
||||||
|
ReleaseDataColumn::DataType => "data_type",
|
||||||
|
ReleaseDataColumn::Options => "options",
|
||||||
|
ReleaseDataColumn::UsedVersionId => "used_version_id",
|
||||||
|
ReleaseDataColumn::UserId => "user_id",
|
||||||
|
ReleaseDataColumn::IsPublished => "is_published",
|
||||||
|
ReleaseDataColumn::CreatedAt => "created_at",
|
||||||
|
ReleaseDataColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for ReleaseDataVersionColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ReleaseDataVersionColumn::Table => "rtsa.release_data_version",
|
||||||
|
ReleaseDataVersionColumn::Id => "id",
|
||||||
|
ReleaseDataVersionColumn::ReleaseDataId => "release_data_id",
|
||||||
|
ReleaseDataVersionColumn::Options => "options",
|
||||||
|
ReleaseDataVersionColumn::Data => "data",
|
||||||
|
ReleaseDataVersionColumn::Description => "description",
|
||||||
|
ReleaseDataVersionColumn::UserId => "user_id",
|
||||||
|
ReleaseDataVersionColumn::CreatedAt => "created_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for ReleaseDataSetColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ReleaseDataSetColumn::Table => "rtsa.release_data_set",
|
||||||
|
ReleaseDataSetColumn::Id => "id",
|
||||||
|
ReleaseDataSetColumn::DataSetType => "data_set_type",
|
||||||
|
ReleaseDataSetColumn::Name => "name",
|
||||||
|
ReleaseDataSetColumn::Description => "description",
|
||||||
|
ReleaseDataSetColumn::Config => "config",
|
||||||
|
ReleaseDataSetColumn::IsPublished => "is_published",
|
||||||
|
ReleaseDataSetColumn::CreatorId => "creator_id",
|
||||||
|
ReleaseDataSetColumn::CreatedAt => "created_at",
|
||||||
|
ReleaseDataSetColumn::UpdaterId => "updater_id",
|
||||||
|
ReleaseDataSetColumn::UpdatedAt => "updated_at",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableColumn for ReleaseDataSetDataColumn {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ReleaseDataSetDataColumn::Table => "rtsa.release_data_set_data",
|
||||||
|
ReleaseDataSetDataColumn::Id => "id",
|
||||||
|
ReleaseDataSetDataColumn::ReleaseDataSetId => "release_data_set_id",
|
||||||
|
ReleaseDataSetDataColumn::ReleaseDataId => "release_data_id",
|
||||||
|
ReleaseDataSetDataColumn::OrderIndex => "order_index",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
crates/rtsa_db/src/password_util.rs
Normal file
71
crates/rtsa_db/src/password_util.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use argon2::{
|
||||||
|
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||||
|
Argon2,
|
||||||
|
};
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use md5::{Digest, Md5};
|
||||||
|
|
||||||
|
use crate::DbAccessError;
|
||||||
|
|
||||||
|
/// 验证密码是否正确
|
||||||
|
pub fn verify_password(password: &str, hash: &str) -> Result<(), DbAccessError> {
|
||||||
|
// Verify password against PHC string.
|
||||||
|
// NOTE: hash params from `parsed_hash` are used instead of what is configured in the
|
||||||
|
// `Argon2` instance.
|
||||||
|
let parsed_hash = PasswordHash::new(hash);
|
||||||
|
match parsed_hash {
|
||||||
|
Ok(parsed_hash) => Argon2::default()
|
||||||
|
.verify_password(password.as_bytes(), &parsed_hash)
|
||||||
|
.map_err(|e| DbAccessError::InvalidArgument(format!("password verify error: {}", e))),
|
||||||
|
Err(e) => Err(DbAccessError::InvalidArgument(format!(
|
||||||
|
"password hash error: {}",
|
||||||
|
e
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash_password(password: &str) -> Result<String, DbAccessError> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
|
||||||
|
// Argon2 with default params (Argon2id v19)
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
|
// Hash password to PHC string ($argon2id$v=19$...)
|
||||||
|
let password_hash = argon2.hash_password(password.as_bytes(), &salt);
|
||||||
|
match password_hash {
|
||||||
|
Ok(ph) => Ok(ph.to_string()),
|
||||||
|
Err(e) => Err(DbAccessError::InvalidArgument(format!(
|
||||||
|
"password hash error: {}",
|
||||||
|
e
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn md5(input: &str) -> String {
|
||||||
|
let mut hasher = Md5::new();
|
||||||
|
hasher.update(input);
|
||||||
|
let result = hasher.finalize();
|
||||||
|
format!("{:x}", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hash_password() {
|
||||||
|
let password = "password";
|
||||||
|
let hash = hash_password(password).unwrap();
|
||||||
|
assert!(verify_password(password, &hash).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_md5() {
|
||||||
|
// let input = "hello";
|
||||||
|
let input = "joylink0503";
|
||||||
|
let output = md5(input);
|
||||||
|
println!("{}", output);
|
||||||
|
// assert_eq!(output, "5d41402abc4b2a76b9719d911017c592");
|
||||||
|
}
|
||||||
|
}
|
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"));
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rtss_dto"
|
name = "rtsa_dto"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
|
prost-types = "0.13"
|
||||||
async-graphql = { version = "7.0.7", features = ["chrono"] }
|
async-graphql = { version = "7.0.7", features = ["chrono"] }
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.13"
|
prost-build = "0.13"
|
59
crates/rtsa_dto/build.rs
Normal file
59
crates/rtsa_dto/build.rs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use prost_build::Config;
|
||||||
|
fn main() {
|
||||||
|
// 获取构建的profile
|
||||||
|
let profile = std::env::var("PROFILE").unwrap();
|
||||||
|
println!("profile: {}", profile);
|
||||||
|
|
||||||
|
// 如果是release模式,不重新生成
|
||||||
|
if profile == "release" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
std::env::set_var(
|
||||||
|
"PROTOC",
|
||||||
|
"../../rtsa-proto-msg/protoc/protoc-27.4-win64/bin/protoc.exe",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Config::new()
|
||||||
|
.out_dir("src/pb")
|
||||||
|
.type_attribute(
|
||||||
|
"common.Role",
|
||||||
|
"#[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(
|
||||||
|
"common.LineType",
|
||||||
|
"#[derive(async_graphql::Enum, serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
"common.IscsStyle",
|
||||||
|
"#[derive(async_graphql::Enum, serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
"common.FeatureType",
|
||||||
|
"#[derive(sqlx::Type, async_graphql::Enum, serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.compile_protos(
|
||||||
|
&[
|
||||||
|
"../../rtsa-proto-msg/src/common.proto",
|
||||||
|
"../../rtsa-proto-msg/src/iscs_graphic_data.proto",
|
||||||
|
"../../rtsa-proto-msg/src/simulation.proto",
|
||||||
|
],
|
||||||
|
&["../../rtsa-proto-msg/src/"],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Run cargo fmt to format the generated code
|
||||||
|
Command::new("cargo")
|
||||||
|
.args(["fmt"])
|
||||||
|
.status()
|
||||||
|
.expect("Failed to run cargo fmt on rtsa-dto");
|
||||||
|
}
|
27
crates/rtsa_dto/src/lib.rs
Normal file
27
crates/rtsa_dto/src/lib.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
mod pb;
|
||||||
|
|
||||||
|
pub use pb::*;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use crate::simulation::{self, operation, SetSpeedParam};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prost_any() {
|
||||||
|
let op = simulation::Operation {
|
||||||
|
otype: simulation::OperationType::SetSpeed as i32,
|
||||||
|
param: Some(operation::Param::SetSpeedParam(SetSpeedParam {
|
||||||
|
speed: 1.0,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
println!("{:?}", op);
|
||||||
|
if let Some(param) = op.param {
|
||||||
|
match param {
|
||||||
|
operation::Param::SetSpeedParam(ssp) => {
|
||||||
|
assert_eq!(ssp.speed, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -86,12 +86,78 @@ pub struct CommonInfo {
|
|||||||
pub transform: ::core::option::Option<Transform>,
|
pub transform: ::core::option::Option<Transform>,
|
||||||
/// 子元素变换
|
/// 子元素变换
|
||||||
#[prost(message, repeated, tag = "4")]
|
#[prost(message, repeated, tag = "4")]
|
||||||
pub children_transform: ::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,
|
||||||
|
/// -----组织用户角色-----
|
||||||
|
/// 组织管理员
|
||||||
|
OrgManager = 11,
|
||||||
|
/// 组织教师
|
||||||
|
OrgTeacher = 12,
|
||||||
|
/// 组织学生
|
||||||
|
OrgStudent = 13,
|
||||||
|
/// 组织访客
|
||||||
|
OrgGuest = 14,
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
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.
|
||||||
|
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),
|
||||||
|
"Role_OrgManager" => Some(Self::OrgManager),
|
||||||
|
"Role_OrgTeacher" => Some(Self::OrgTeacher),
|
||||||
|
"Role_OrgStudent" => Some(Self::OrgStudent),
|
||||||
|
"Role_OrgGuest" => Some(Self::OrgGuest),
|
||||||
|
_ => 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 +174,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 +186,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,23 +194,124 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// 线路类型
|
||||||
|
#[derive(
|
||||||
|
async_graphql::Enum,
|
||||||
|
serde::Serialize,
|
||||||
|
serde::Deserialize,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Hash,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
::prost::Enumeration,
|
||||||
|
)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum LineType {
|
||||||
|
/// 未知
|
||||||
|
Unknown = 0,
|
||||||
|
/// 城市轨道交通
|
||||||
|
Ur = 1,
|
||||||
|
/// 城际轨道交通
|
||||||
|
Ir = 2,
|
||||||
|
/// 市域轨道交通
|
||||||
|
Cr = 3,
|
||||||
|
}
|
||||||
|
impl LineType {
|
||||||
|
/// 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 {
|
||||||
|
LineType::Unknown => "LineType_Unknown",
|
||||||
|
LineType::Ur => "LineType_Ur",
|
||||||
|
LineType::Ir => "LineType_Ir",
|
||||||
|
LineType::Cr => "LineType_Cr",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||||
|
match value {
|
||||||
|
"LineType_Unknown" => Some(Self::Unknown),
|
||||||
|
"LineType_Ur" => Some(Self::Ur),
|
||||||
|
"LineType_Ir" => Some(Self::Ir),
|
||||||
|
"LineType_Cr" => Some(Self::Cr),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(
|
||||||
|
async_graphql::Enum,
|
||||||
|
serde::Serialize,
|
||||||
|
serde::Deserialize,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Hash,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
::prost::Enumeration,
|
||||||
|
)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum IscsStyle {
|
||||||
|
/// 未知
|
||||||
|
Unknown = 0,
|
||||||
|
/// 达实智能(福州一号线)
|
||||||
|
DaShiZhiNeng = 1,
|
||||||
|
}
|
||||||
|
impl IscsStyle {
|
||||||
|
/// 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 {
|
||||||
|
IscsStyle::Unknown => "IscsStyle_Unknown",
|
||||||
|
IscsStyle::DaShiZhiNeng => "IscsStyle_DaShiZhiNeng",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||||
|
match value {
|
||||||
|
"IscsStyle_Unknown" => Some(Self::Unknown),
|
||||||
|
"IscsStyle_DaShiZhiNeng" => Some(Self::DaShiZhiNeng),
|
||||||
|
_ => 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,
|
||||||
}
|
}
|
||||||
@ -162,7 +323,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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,7 +331,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,
|
||||||
}
|
}
|
662
crates/rtsa_dto/src/pb/iscs_graphic_data.rs
Normal file
662
crates/rtsa_dto/src/pb/iscs_graphic_data.rs
Normal file
@ -0,0 +1,662 @@
|
|||||||
|
// This file is @generated by prost-build.
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct IscsGraphicStorage {
|
||||||
|
#[prost(message, repeated, tag = "1")]
|
||||||
|
pub cctv_of_equipment_layout_storages: ::prost::alloc::vec::Vec<
|
||||||
|
CctvOfEquipmentLayoutStorage,
|
||||||
|
>,
|
||||||
|
#[prost(message, repeated, tag = "2")]
|
||||||
|
pub fas_of_platform_alarm_storages: ::prost::alloc::vec::Vec<
|
||||||
|
FasOfPlatformAlarmStorage,
|
||||||
|
>,
|
||||||
|
#[prost(message, repeated, tag = "3")]
|
||||||
|
pub bas_of_escalator_storages: ::prost::alloc::vec::Vec<BasOfEscalatorStorage>,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct UniqueIdOfStationLayout {
|
||||||
|
/// 城市
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub city: ::prost::alloc::string::String,
|
||||||
|
/// 线路号
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub line_id: ::prost::alloc::string::String,
|
||||||
|
/// 地图的公里标主坐标系
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub main_coordinate_system: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct CommonGraphicStorage {
|
||||||
|
#[prost(message, repeated, tag = "1")]
|
||||||
|
pub arrows: ::prost::alloc::vec::Vec<Arrow>,
|
||||||
|
#[prost(message, repeated, tag = "2")]
|
||||||
|
pub texts: ::prost::alloc::vec::Vec<Text>,
|
||||||
|
#[prost(message, repeated, tag = "3")]
|
||||||
|
pub rects: ::prost::alloc::vec::Vec<Rect>,
|
||||||
|
#[prost(message, repeated, tag = "4")]
|
||||||
|
pub lines: ::prost::alloc::vec::Vec<Line>,
|
||||||
|
#[prost(message, repeated, tag = "5")]
|
||||||
|
pub circles: ::prost::alloc::vec::Vec<Circle>,
|
||||||
|
#[prost(message, repeated, tag = "6")]
|
||||||
|
pub buttons: ::prost::alloc::vec::Vec<Button>,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Arrow {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
#[prost(message, repeated, tag = "3")]
|
||||||
|
pub points: ::prost::alloc::vec::Vec<super::common::Point>,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Text {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub content: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub color: ::prost::alloc::string::String,
|
||||||
|
#[prost(int32, tag = "5")]
|
||||||
|
pub font_size: i32,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Rect {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
/// 线宽
|
||||||
|
#[prost(int32, tag = "3")]
|
||||||
|
pub line_width: i32,
|
||||||
|
/// 线色
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub line_color: ::prost::alloc::string::String,
|
||||||
|
/// 宽度
|
||||||
|
#[prost(float, tag = "5")]
|
||||||
|
pub width: f32,
|
||||||
|
/// 高度
|
||||||
|
#[prost(float, tag = "6")]
|
||||||
|
pub height: f32,
|
||||||
|
/// 圆角半径
|
||||||
|
#[prost(int32, tag = "7")]
|
||||||
|
pub radius: i32,
|
||||||
|
/// 画第一个点的坐标
|
||||||
|
#[prost(message, optional, tag = "8")]
|
||||||
|
pub point: ::core::option::Option<super::common::Point>,
|
||||||
|
/// 填充色
|
||||||
|
#[prost(string, tag = "9")]
|
||||||
|
pub fill_color: ::prost::alloc::string::String,
|
||||||
|
/// 透明度
|
||||||
|
#[prost(float, tag = "10")]
|
||||||
|
pub alpha: f32,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Line {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
/// 编号
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
/// 点列表
|
||||||
|
#[prost(message, repeated, tag = "3")]
|
||||||
|
pub points: ::prost::alloc::vec::Vec<super::common::Point>,
|
||||||
|
/// 是否曲线
|
||||||
|
#[prost(bool, tag = "4")]
|
||||||
|
pub is_curve: bool,
|
||||||
|
/// 曲线分段数
|
||||||
|
#[prost(int32, tag = "5")]
|
||||||
|
pub segments_count: i32,
|
||||||
|
/// 线宽
|
||||||
|
#[prost(int32, tag = "6")]
|
||||||
|
pub line_width: i32,
|
||||||
|
/// 线色
|
||||||
|
#[prost(string, tag = "7")]
|
||||||
|
pub line_color: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Circle {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
#[prost(message, optional, tag = "3")]
|
||||||
|
pub position: ::core::option::Option<super::common::Point>,
|
||||||
|
#[prost(float, tag = "4")]
|
||||||
|
pub radius: f32,
|
||||||
|
#[prost(int32, tag = "5")]
|
||||||
|
pub line_width: i32,
|
||||||
|
#[prost(string, tag = "6")]
|
||||||
|
pub line_color: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "7")]
|
||||||
|
pub fill_color: ::prost::alloc::string::String,
|
||||||
|
#[prost(float, tag = "8")]
|
||||||
|
pub alpha: f32,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Button {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub code_color: ::prost::alloc::string::String,
|
||||||
|
#[prost(int32, tag = "4")]
|
||||||
|
pub code_font_size: i32,
|
||||||
|
/// 所属ISCS子菜单
|
||||||
|
#[prost(string, tag = "5")]
|
||||||
|
pub belong_sub_menu: ::prost::alloc::string::String,
|
||||||
|
/// 类似icon
|
||||||
|
#[prost(enumeration = "button::ButtonType", tag = "6")]
|
||||||
|
pub button_type: i32,
|
||||||
|
/// 宽度
|
||||||
|
#[prost(float, tag = "7")]
|
||||||
|
pub width: f32,
|
||||||
|
/// 高度
|
||||||
|
#[prost(float, tag = "8")]
|
||||||
|
pub height: f32,
|
||||||
|
/// 圆角半径_可变圆
|
||||||
|
#[prost(int32, tag = "9")]
|
||||||
|
pub radius: i32,
|
||||||
|
/// 填充色
|
||||||
|
#[prost(string, tag = "10")]
|
||||||
|
pub fill_color: ::prost::alloc::string::String,
|
||||||
|
/// 透明度
|
||||||
|
#[prost(float, tag = "11")]
|
||||||
|
pub alpha: f32,
|
||||||
|
}
|
||||||
|
/// Nested message and enum types in `Button`.
|
||||||
|
pub mod button {
|
||||||
|
#[derive(
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Hash,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
::prost::Enumeration
|
||||||
|
)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum ButtonType {
|
||||||
|
/// 没有Icon
|
||||||
|
NoIcon = 0,
|
||||||
|
/// 监控方形
|
||||||
|
CctvRect = 1,
|
||||||
|
/// 监控样子的按钮
|
||||||
|
CctvMonitor = 2,
|
||||||
|
/// 半圆样子的按钮
|
||||||
|
CctvSemicircle = 3,
|
||||||
|
}
|
||||||
|
impl ButtonType {
|
||||||
|
/// 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 {
|
||||||
|
ButtonType::NoIcon => "noIcon",
|
||||||
|
ButtonType::CctvRect => "cctvRect",
|
||||||
|
ButtonType::CctvMonitor => "cctvMonitor",
|
||||||
|
ButtonType::CctvSemicircle => "cctvSemicircle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||||
|
match value {
|
||||||
|
"noIcon" => Some(Self::NoIcon),
|
||||||
|
"cctvRect" => Some(Self::CctvRect),
|
||||||
|
"cctvMonitor" => Some(Self::CctvMonitor),
|
||||||
|
"cctvSemicircle" => Some(Self::CctvSemicircle),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 手动报警按钮
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct ManualAlarmButton {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 消防栓报警按钮
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct HydrantAlarmButton {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 气体灭火
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct GasExtinguishing {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 烟雾探测器
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct SmokeDetector {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 温度探测器
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct TemperatureDetector {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct BasOfEscalatorStorage {
|
||||||
|
#[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, optional, tag = "3")]
|
||||||
|
pub common_graphic_storage: ::core::option::Option<CommonGraphicStorage>,
|
||||||
|
#[prost(message, repeated, tag = "4")]
|
||||||
|
pub escalators: ::prost::alloc::vec::Vec<Escalator>,
|
||||||
|
#[prost(message, repeated, tag = "5")]
|
||||||
|
pub vertical_elevators: ::prost::alloc::vec::Vec<VerticalElevator>,
|
||||||
|
}
|
||||||
|
/// 自动扶梯
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Escalator {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 垂直电梯
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct VerticalElevator {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct CctvOfEquipmentLayoutStorage {
|
||||||
|
#[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, optional, tag = "3")]
|
||||||
|
pub common_graphic_storage: ::core::option::Option<CommonGraphicStorage>,
|
||||||
|
/// 分层
|
||||||
|
#[prost(enumeration = "cctv_of_equipment_layout_storage::LayerType", tag = "4")]
|
||||||
|
pub layer: i32,
|
||||||
|
}
|
||||||
|
/// Nested message and enum types in `CCTVOfEquipmentLayoutStorage`.
|
||||||
|
pub mod cctv_of_equipment_layout_storage {
|
||||||
|
#[derive(
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Hash,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
::prost::Enumeration
|
||||||
|
)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum LayerType {
|
||||||
|
/// 站厅
|
||||||
|
StationHall = 0,
|
||||||
|
/// 站台
|
||||||
|
Platform = 1,
|
||||||
|
/// 云台
|
||||||
|
Ptz = 2,
|
||||||
|
}
|
||||||
|
impl LayerType {
|
||||||
|
/// 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 {
|
||||||
|
LayerType::StationHall => "StationHall",
|
||||||
|
LayerType::Platform => "platform",
|
||||||
|
LayerType::Ptz => "PTZ",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||||
|
match value {
|
||||||
|
"StationHall" => Some(Self::StationHall),
|
||||||
|
"platform" => Some(Self::Platform),
|
||||||
|
"PTZ" => Some(Self::Ptz),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct FasOfPlatformAlarmStorage {
|
||||||
|
#[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, optional, tag = "3")]
|
||||||
|
pub common_graphic_storage: ::core::option::Option<CommonGraphicStorage>,
|
||||||
|
/// 分区
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub partition: ::prost::alloc::string::String,
|
||||||
|
#[prost(message, repeated, tag = "5")]
|
||||||
|
pub fas_failure_control_hosts: ::prost::alloc::vec::Vec<FasFailureControlHost>,
|
||||||
|
#[prost(message, repeated, tag = "6")]
|
||||||
|
pub fas_alarms: ::prost::alloc::vec::Vec<FasAlarm>,
|
||||||
|
#[prost(message, repeated, tag = "7")]
|
||||||
|
pub manual_alarm_buttons: ::prost::alloc::vec::Vec<ManualAlarmButton>,
|
||||||
|
#[prost(message, repeated, tag = "8")]
|
||||||
|
pub hydrant_alarm_buttons: ::prost::alloc::vec::Vec<HydrantAlarmButton>,
|
||||||
|
#[prost(message, repeated, tag = "9")]
|
||||||
|
pub gas_extinguishings: ::prost::alloc::vec::Vec<GasExtinguishing>,
|
||||||
|
#[prost(message, repeated, tag = "10")]
|
||||||
|
pub smoke_detectors: ::prost::alloc::vec::Vec<SmokeDetector>,
|
||||||
|
#[prost(message, repeated, tag = "11")]
|
||||||
|
pub temperature_detectors: ::prost::alloc::vec::Vec<TemperatureDetector>,
|
||||||
|
#[prost(message, repeated, tag = "12")]
|
||||||
|
pub fire_shutters: ::prost::alloc::vec::Vec<FireShutter>,
|
||||||
|
#[prost(message, repeated, tag = "13")]
|
||||||
|
pub fire_pumps: ::prost::alloc::vec::Vec<FirePump>,
|
||||||
|
#[prost(message, repeated, tag = "14")]
|
||||||
|
pub spray_pumps: ::prost::alloc::vec::Vec<SprayPump>,
|
||||||
|
#[prost(message, repeated, tag = "15")]
|
||||||
|
pub stabilized_pressure_pumps: ::prost::alloc::vec::Vec<StabilizedPressurePump>,
|
||||||
|
#[prost(message, repeated, tag = "16")]
|
||||||
|
pub acs: ::prost::alloc::vec::Vec<Acs>,
|
||||||
|
#[prost(message, repeated, tag = "17")]
|
||||||
|
pub afc: ::prost::alloc::vec::Vec<Afc>,
|
||||||
|
#[prost(message, repeated, tag = "18")]
|
||||||
|
pub non_fire_power_supplies: ::prost::alloc::vec::Vec<NonFirePowerSupply>,
|
||||||
|
#[prost(message, repeated, tag = "19")]
|
||||||
|
pub water_flow_indicators: ::prost::alloc::vec::Vec<WaterFlowIndicator>,
|
||||||
|
#[prost(message, repeated, tag = "20")]
|
||||||
|
pub signal_butterfly_valves: ::prost::alloc::vec::Vec<SignalButterflyValve>,
|
||||||
|
#[prost(message, repeated, tag = "21")]
|
||||||
|
pub pressure_switches: ::prost::alloc::vec::Vec<PressureSwitch>,
|
||||||
|
#[prost(message, repeated, tag = "22")]
|
||||||
|
pub fault_valves: ::prost::alloc::vec::Vec<FaultValve>,
|
||||||
|
#[prost(message, repeated, tag = "23")]
|
||||||
|
pub start_pump_buttons: ::prost::alloc::vec::Vec<StartPumpButton>,
|
||||||
|
#[prost(message, repeated, tag = "24")]
|
||||||
|
pub temperature_cables: ::prost::alloc::vec::Vec<TemperatureCable>,
|
||||||
|
#[prost(message, repeated, tag = "25")]
|
||||||
|
pub emergency_lightings: ::prost::alloc::vec::Vec<EmergencyLighting>,
|
||||||
|
#[prost(message, repeated, tag = "26")]
|
||||||
|
pub elevator_lift_to_tops: ::prost::alloc::vec::Vec<ElevatorLiftToTop>,
|
||||||
|
#[prost(message, repeated, tag = "27")]
|
||||||
|
pub electric_butterfly_valves: ::prost::alloc::vec::Vec<ElectricButterflyValve>,
|
||||||
|
#[prost(message, repeated, tag = "28")]
|
||||||
|
pub fire_valves: ::prost::alloc::vec::Vec<FireValve>,
|
||||||
|
#[prost(message, repeated, tag = "29")]
|
||||||
|
pub electric_fire_extinguishing_valves: ::prost::alloc::vec::Vec<
|
||||||
|
ElectricFireExtinguishingValve,
|
||||||
|
>,
|
||||||
|
#[prost(message, repeated, tag = "30")]
|
||||||
|
pub fire_intercommunication_signals: ::prost::alloc::vec::Vec<
|
||||||
|
FireIntercommunicationSignal,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
/// 火灾故障控制主机
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct FasFailureControlHost {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 警铃
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct FasAlarm {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 防火卷帘
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct FireShutter {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
#[prost(enumeration = "fire_shutter::ShutterType", tag = "3")]
|
||||||
|
pub r#type: i32,
|
||||||
|
}
|
||||||
|
/// Nested message and enum types in `FireShutter`.
|
||||||
|
pub mod fire_shutter {
|
||||||
|
#[derive(
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Hash,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
::prost::Enumeration
|
||||||
|
)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum ShutterType {
|
||||||
|
/// 隔断型
|
||||||
|
Partition = 0,
|
||||||
|
/// 疏散型
|
||||||
|
Dispersal = 1,
|
||||||
|
}
|
||||||
|
impl ShutterType {
|
||||||
|
/// 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 {
|
||||||
|
ShutterType::Partition => "partition",
|
||||||
|
ShutterType::Dispersal => "dispersal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||||
|
match value {
|
||||||
|
"partition" => Some(Self::Partition),
|
||||||
|
"dispersal" => Some(Self::Dispersal),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 消防泵
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct FirePump {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 喷淋泵
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct SprayPump {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 稳压泵
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct StabilizedPressurePump {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// ACS
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Acs {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// AFC
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Afc {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 非消防电源
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct NonFirePowerSupply {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 水流指示器
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct WaterFlowIndicator {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 信号蝶阀
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct SignalButterflyValve {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 压力开关
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct PressureSwitch {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 故障阀
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct FaultValve {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 启泵按钮
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct StartPumpButton {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 感温电缆
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct TemperatureCable {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 应急照明
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct EmergencyLighting {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 电梯归首
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct ElevatorLiftToTop {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 电动蝶阀
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct ElectricButterflyValve {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 防火阀
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct FireValve {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 电动防烟防火阀
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct ElectricFireExtinguishingValve {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// 火灾互联互通信号
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct FireIntercommunicationSignal {
|
||||||
|
#[prost(message, optional, tag = "1")]
|
||||||
|
pub common: ::core::option::Option<super::common::CommonInfo>,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub code: ::prost::alloc::string::String,
|
||||||
|
}
|
2
crates/rtsa_dto/src/pb/mod.rs
Normal file
2
crates/rtsa_dto/src/pb/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod common;
|
||||||
|
pub mod simulation;
|
77
crates/rtsa_dto/src/pb/simulation.rs
Normal file
77
crates/rtsa_dto/src/pb/simulation.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// This file is @generated by prost-build.
|
||||||
|
/// 仿真操作
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Operation {
|
||||||
|
/// 操作类型
|
||||||
|
#[prost(enumeration = "OperationType", tag = "1")]
|
||||||
|
pub otype: i32,
|
||||||
|
/// 操作参数
|
||||||
|
#[prost(oneof = "operation::Param", tags = "2")]
|
||||||
|
pub param: ::core::option::Option<operation::Param>,
|
||||||
|
}
|
||||||
|
/// Nested message and enum types in `Operation`.
|
||||||
|
pub mod operation {
|
||||||
|
/// 操作参数
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, Copy, PartialEq, ::prost::Oneof)]
|
||||||
|
pub enum Param {
|
||||||
|
/// 设置仿真运行速度参数
|
||||||
|
#[prost(message, tag = "2")]
|
||||||
|
SetSpeedParam(super::SetSpeedParam),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 设置仿真运行速度参数
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||||
|
pub struct SetSpeedParam {
|
||||||
|
/// 运行速度
|
||||||
|
#[prost(float, tag = "1")]
|
||||||
|
pub speed: f32,
|
||||||
|
}
|
||||||
|
/// 仿真操作类型
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum OperationType {
|
||||||
|
/// 未知
|
||||||
|
Unknown = 0,
|
||||||
|
/// -------仿真控制操作--------
|
||||||
|
/// 暂停
|
||||||
|
Pause = 1,
|
||||||
|
/// 恢复运行
|
||||||
|
Unpause = 2,
|
||||||
|
/// 重置
|
||||||
|
Reset = 3,
|
||||||
|
/// 设置运行速度
|
||||||
|
SetSpeed = 4,
|
||||||
|
/// 销毁
|
||||||
|
Destroy = 5,
|
||||||
|
}
|
||||||
|
impl OperationType {
|
||||||
|
/// 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 {
|
||||||
|
OperationType::Unknown => "Unknown",
|
||||||
|
OperationType::Pause => "Pause",
|
||||||
|
OperationType::Unpause => "Unpause",
|
||||||
|
OperationType::Reset => "Reset",
|
||||||
|
OperationType::SetSpeed => "SetSpeed",
|
||||||
|
OperationType::Destroy => "Destroy",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||||
|
match value {
|
||||||
|
"Unknown" => Some(Self::Unknown),
|
||||||
|
"Pause" => Some(Self::Pause),
|
||||||
|
"Unpause" => Some(Self::Unpause),
|
||||||
|
"Reset" => Some(Self::Reset),
|
||||||
|
"SetSpeed" => Some(Self::SetSpeed),
|
||||||
|
"Destroy" => Some(Self::Destroy),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rtss_log"
|
name = "rtsa_log"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
15
crates/rtsa_mqtt/Cargo.toml
Normal file
15
crates/rtsa_mqtt/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "rtsa_mqtt"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rumqttc = { version = "0.24.0", features = ["url"] }
|
||||||
|
tokio = { workspace = true, features = ["sync"] }
|
||||||
|
async-trait = { workspace = true }
|
||||||
|
bytes = { workspace = true }
|
||||||
|
lazy_static = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tower = { workspace = true }
|
||||||
|
|
||||||
|
rtsa_log = { path = "../rtsa_log" }
|
16
crates/rtsa_mqtt/src/error.rs
Normal file
16
crates/rtsa_mqtt/src/error.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use rumqttc::v5::ClientError;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum MqttClientError {
|
||||||
|
#[error("未知的Mqtt客户端错误")]
|
||||||
|
Unknown,
|
||||||
|
#[error("客户端已设置")]
|
||||||
|
AlreadySet,
|
||||||
|
#[error("rumqttc 错误: {0}")]
|
||||||
|
ClientError(#[from] ClientError),
|
||||||
|
#[error("全局客户端未设置")]
|
||||||
|
NoClient,
|
||||||
|
#[error("请求超时")]
|
||||||
|
RequestTimeout,
|
||||||
|
}
|
512
crates/rtsa_mqtt/src/lib.rs
Normal file
512
crates/rtsa_mqtt/src/lib.rs
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
use core::panic;
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
Arc, Mutex,
|
||||||
|
},
|
||||||
|
task::Waker,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use rtsa_log::tracing::{error, info, trace};
|
||||||
|
use rumqttc::{
|
||||||
|
v5::{
|
||||||
|
mqttbytes::{
|
||||||
|
v5::{Packet, Publish, PublishProperties},
|
||||||
|
QoS,
|
||||||
|
},
|
||||||
|
AsyncClient, Event, EventLoop, MqttOptions,
|
||||||
|
},
|
||||||
|
Outgoing,
|
||||||
|
};
|
||||||
|
use service::{Handler, MqttRequest, MqttRouter};
|
||||||
|
use tokio::{sync::oneshot, time::timeout};
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
use error::MqttClientError;
|
||||||
|
mod service;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
/// 全局静态MqttClient实例
|
||||||
|
/// 使用注意事项:
|
||||||
|
/// 每次订阅/发布/请求时都通过get_global_mqtt_client获取新的实例,否则可能会出现死锁
|
||||||
|
static ref MQTT_CLIENT: tokio::sync::Mutex<Option<MqttClient>> = tokio::sync::Mutex::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化全局MqttClient实例
|
||||||
|
pub async fn init_global_mqtt_client(
|
||||||
|
mut options: MqttClientOptions,
|
||||||
|
) -> Result<(), MqttClientError> {
|
||||||
|
let client = options.build();
|
||||||
|
set_global_mqtt_client(client).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置全局MqttClient实例
|
||||||
|
pub async fn set_global_mqtt_client(client: MqttClient) -> Result<(), MqttClientError> {
|
||||||
|
let mut mqtt_client = MQTT_CLIENT.lock().await;
|
||||||
|
if mqtt_client.is_some() {
|
||||||
|
return Err(MqttClientError::AlreadySet);
|
||||||
|
}
|
||||||
|
*mqtt_client = Some(client);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取全局MqttClient实例
|
||||||
|
pub async fn get_global_mqtt_client() -> MqttClient {
|
||||||
|
let mqtt_client = MQTT_CLIENT.lock().await;
|
||||||
|
if let Some(client) = mqtt_client.as_ref() {
|
||||||
|
return client.clone();
|
||||||
|
}
|
||||||
|
panic!("MqttClient未初始化: 使用init_global_mqtt_client初始化,或者在main函数中调用set_global_mqtt_client设置");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MqttClientOptions {
|
||||||
|
id: String,
|
||||||
|
options: MqttOptions,
|
||||||
|
/// mqtt客户端请求队列的最大容量
|
||||||
|
max_cap: usize,
|
||||||
|
request_timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttClientOptions {
|
||||||
|
pub fn new(id: &str, url: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.to_string(),
|
||||||
|
options: MqttOptions::parse_url(format!("{}?client_id={}", url, id))
|
||||||
|
.expect("解析mqtt url失败"),
|
||||||
|
max_cap: 30,
|
||||||
|
request_timeout: Duration::from_secs(5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_max_cap(mut self, max_cap: usize) -> Self {
|
||||||
|
self.max_cap = max_cap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_request_timeout(mut self, timeout: Duration) -> Self {
|
||||||
|
self.request_timeout = timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_credentials(mut self, username: &str, password: &str) -> Self {
|
||||||
|
self.options.set_credentials(username, password);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&mut self) -> MqttClient {
|
||||||
|
self.options.set_keep_alive(Duration::from_secs(10));
|
||||||
|
let (client, eventloop) = AsyncClient::new(self.options.clone(), self.max_cap);
|
||||||
|
|
||||||
|
let cli = MqttClient {
|
||||||
|
id: self.id.clone(),
|
||||||
|
request_timeout: self.request_timeout,
|
||||||
|
client,
|
||||||
|
request_id: Arc::new(AtomicU64::new(0)),
|
||||||
|
router: MqttRouter::new(),
|
||||||
|
};
|
||||||
|
cli.run_async(eventloop);
|
||||||
|
cli
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MQTT客户端
|
||||||
|
/// id: 客户端ID,从数据库的id序列中获取
|
||||||
|
/// 客户端具有的功能:
|
||||||
|
/// 1. 启动
|
||||||
|
/// 2. 订阅
|
||||||
|
/// 3. 发布
|
||||||
|
/// 4. 实现类似http的请求相应功能
|
||||||
|
/// 5. 断开连接
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MqttClient {
|
||||||
|
id: String,
|
||||||
|
/// 全局的请求超时时间
|
||||||
|
request_timeout: Duration,
|
||||||
|
client: AsyncClient,
|
||||||
|
request_id: Arc<AtomicU64>,
|
||||||
|
router: MqttRouter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttClient {
|
||||||
|
pub async fn clear(&self) -> Result<(), MqttClientError> {
|
||||||
|
self.client.disconnect().await?;
|
||||||
|
// 清空订阅处理器
|
||||||
|
self.router.clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish(
|
||||||
|
&self,
|
||||||
|
topic: &str,
|
||||||
|
qos: QoS,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
) -> Result<(), MqttClientError> {
|
||||||
|
self.client.publish(topic, qos, false, payload).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_route<H>(&self, topic: &str, handler: H, qos: QoS)
|
||||||
|
where
|
||||||
|
H: Handler + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.client.subscribe(topic, qos).await.unwrap();
|
||||||
|
self.router.add_route(topic, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_route(&self, topic: &str) {
|
||||||
|
self.client.unsubscribe(topic).await.unwrap();
|
||||||
|
self.router.remove_route(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_request_id(&self) -> u64 {
|
||||||
|
self.request_id.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_request(
|
||||||
|
&self,
|
||||||
|
req: Request,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<Response, MqttClientError> {
|
||||||
|
// 订阅响应主题
|
||||||
|
let response_topic = format!("{}/{}/resp/{}", self.id, req.topic, self.next_request_id());
|
||||||
|
// 创建请求future
|
||||||
|
let response_future = MqttResponseFuture::new(&response_topic, timeout);
|
||||||
|
// 添加响应处理器
|
||||||
|
self.add_route(&response_topic, response_future.clone(), QoS::ExactlyOnce)
|
||||||
|
.await;
|
||||||
|
// 发布请求
|
||||||
|
let property = PublishProperties {
|
||||||
|
response_topic: Some(response_topic.clone()),
|
||||||
|
..req.properties.unwrap_or_default()
|
||||||
|
};
|
||||||
|
self.client
|
||||||
|
.publish_with_properties(req.topic, req.qos, false, req.payload, property)
|
||||||
|
.await?;
|
||||||
|
// 等待响应
|
||||||
|
let resp = response_future.await;
|
||||||
|
// 注销响应处理器并取消订阅
|
||||||
|
self.remove_route(&response_topic).await;
|
||||||
|
if resp.is_timeout() {
|
||||||
|
return Err(MqttClientError::RequestTimeout);
|
||||||
|
}
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送请求并等待响应
|
||||||
|
pub async fn request(&self, req: Request) -> Result<Response, MqttClientError> {
|
||||||
|
self.handle_request(req, self.request_timeout).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发送请求并等待响应,指定响应超时时间
|
||||||
|
/// 响应超时时间为0时表示永不超时
|
||||||
|
pub async fn request_with_timeout(
|
||||||
|
&self,
|
||||||
|
req: Request,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Result<Response, MqttClientError> {
|
||||||
|
self.handle_request(req, timeout).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_async(&self, eventloop: EventLoop) {
|
||||||
|
let cli = self.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
cli.run(eventloop).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, mut eventloop: EventLoop) {
|
||||||
|
while let Ok(notification) = eventloop.poll().await {
|
||||||
|
match notification {
|
||||||
|
Event::Incoming(Packet::Publish(publish)) => {
|
||||||
|
trace!("Received message: {:?}", publish);
|
||||||
|
let this = self.clone();
|
||||||
|
let router = self.router.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let response_topic = publish
|
||||||
|
.properties
|
||||||
|
.clone()
|
||||||
|
.and_then(|p| p.response_topic.clone());
|
||||||
|
if let Some(resp) = router.handle_request(MqttRequest::new(publish)).await {
|
||||||
|
if let Some(r_topic) = response_topic {
|
||||||
|
this.publish(&r_topic, QoS::AtMostOnce, resp.payload.to_vec())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Event::Outgoing(Outgoing::Disconnect) => {
|
||||||
|
info!("Disconnected to the broker");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Event::Incoming(Packet::Disconnect(disconnect)) => {
|
||||||
|
info!("Disconnected from the broker: {:?}", disconnect);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
trace!("Unhandled event: {:?}", notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Request {
|
||||||
|
topic: String,
|
||||||
|
qos: QoS,
|
||||||
|
payload: Bytes,
|
||||||
|
properties: Option<PublishProperties>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
pub fn new(topic: &str, payload: Bytes) -> Self {
|
||||||
|
Self {
|
||||||
|
topic: topic.to_string(),
|
||||||
|
qos: QoS::AtMostOnce,
|
||||||
|
payload,
|
||||||
|
properties: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_qos(mut self, qos: QoS) -> Self {
|
||||||
|
self.qos = qos;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_properties(mut self, properties: PublishProperties) -> Self {
|
||||||
|
self.properties = Some(properties);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub enum MqttResponseState {
|
||||||
|
Waiting,
|
||||||
|
Received,
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MQTT请求响应
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Response {
|
||||||
|
state: Arc<Mutex<MqttResponseState>>,
|
||||||
|
response: Arc<Mutex<Publish>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Response {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: Arc::new(Mutex::new(MqttResponseState::Waiting)),
|
||||||
|
response: Arc::new(Mutex::new(Publish::default())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_waiting(&self) -> bool {
|
||||||
|
*self.state.lock().unwrap() == MqttResponseState::Waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_received(&self) -> bool {
|
||||||
|
*self.state.lock().unwrap() == MqttResponseState::Received
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_timeout(&self) -> bool {
|
||||||
|
*self.state.lock().unwrap() == MqttResponseState::Timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_timeout(&self) {
|
||||||
|
*self.state.lock().unwrap() = MqttResponseState::Timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&self, response: Publish) {
|
||||||
|
*self.state.lock().unwrap() = MqttResponseState::Received;
|
||||||
|
*self.response.lock().unwrap() = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> Publish {
|
||||||
|
self.response.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MQTT响应Future
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MqttResponseFuture {
|
||||||
|
pub start_time: std::time::Instant,
|
||||||
|
timeout: Duration,
|
||||||
|
tx: Arc<Mutex<Option<oneshot::Sender<()>>>>,
|
||||||
|
waker: Arc<Mutex<Option<Waker>>>,
|
||||||
|
response_topic: String,
|
||||||
|
response: Response,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttResponseFuture {
|
||||||
|
pub fn new(response_topic: &str, timeout: Duration) -> Self {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let r = Self {
|
||||||
|
start_time: std::time::Instant::now(),
|
||||||
|
timeout,
|
||||||
|
tx: Arc::new(Mutex::new(Some(tx))),
|
||||||
|
waker: Arc::new(Mutex::new(None)),
|
||||||
|
response_topic: response_topic.to_string(),
|
||||||
|
response: Response::new(),
|
||||||
|
};
|
||||||
|
// 启动超时检查
|
||||||
|
r.start_timeout_monitor(rx);
|
||||||
|
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动超时监控任务逻辑
|
||||||
|
fn start_timeout_monitor(&self, rx: oneshot::Receiver<()>) {
|
||||||
|
if self.timeout.as_millis() == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response = self.response.clone();
|
||||||
|
let response_topic = self.response_topic.clone();
|
||||||
|
let duration = self.timeout;
|
||||||
|
let waker = self.waker.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if (timeout(duration, rx).await).is_err() {
|
||||||
|
error!("Mqtt response timeout: {:?}", response_topic);
|
||||||
|
response.set_timeout();
|
||||||
|
if let Some(waker) = waker.lock().unwrap().take() {
|
||||||
|
waker.wake();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler for MqttResponseFuture {
|
||||||
|
fn handle(
|
||||||
|
&self,
|
||||||
|
req: MqttRequest,
|
||||||
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Option<service::MqttResponse>> + Send>>
|
||||||
|
{
|
||||||
|
let topic = req.topic();
|
||||||
|
trace!("MqttResponseFuture handle: {:?}", topic);
|
||||||
|
if topic == self.response_topic {
|
||||||
|
self.response.set(req.get());
|
||||||
|
if let Some(tx) = self.tx.lock().unwrap().take() {
|
||||||
|
tx.send(())
|
||||||
|
.expect("Send Mqtt response timeout signal failed");
|
||||||
|
}
|
||||||
|
if let Some(waker) = self.waker.lock().unwrap().take() {
|
||||||
|
waker.wake();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box::pin(async { None })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::future::Future for MqttResponseFuture {
|
||||||
|
type Output = Response;
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
self: std::pin::Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<Self::Output> {
|
||||||
|
if self.response.is_waiting() {
|
||||||
|
trace!(
|
||||||
|
"topic={} Response future poll waiting...",
|
||||||
|
self.response_topic
|
||||||
|
);
|
||||||
|
self.waker.lock().unwrap().replace(cx.waker().clone());
|
||||||
|
std::task::Poll::Pending
|
||||||
|
} else {
|
||||||
|
trace!(
|
||||||
|
"topic={} Response future poll ready: {:?}",
|
||||||
|
self.response_topic,
|
||||||
|
self.response
|
||||||
|
);
|
||||||
|
std::task::Poll::Ready(self.response.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rtsa_log::tracing::Level;
|
||||||
|
|
||||||
|
use tokio::time::Duration;
|
||||||
|
|
||||||
|
fn create_mqtt_options() -> MqttClientOptions {
|
||||||
|
MqttClientOptions::new("rtsa_test1", "tcp://localhost:1883")
|
||||||
|
.set_credentials("rtsa", "Joylink@0503")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_mqtt_client_initialization() {
|
||||||
|
let options = create_mqtt_options();
|
||||||
|
let result = init_global_mqtt_client(options).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_mqtt_client_publish() {
|
||||||
|
let options = create_mqtt_options();
|
||||||
|
init_global_mqtt_client(options).await.unwrap();
|
||||||
|
let mqtt_client = get_global_mqtt_client().await;
|
||||||
|
let result = mqtt_client
|
||||||
|
.publish("test/topic", QoS::AtLeastOnce, b"Hello, MQTT!".to_vec())
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_mqtt_client_request_response() {
|
||||||
|
rtsa_log::Logging::default().with_level(Level::TRACE).init();
|
||||||
|
let options = create_mqtt_options();
|
||||||
|
init_global_mqtt_client(options).await.unwrap();
|
||||||
|
let mqtt_client = get_global_mqtt_client().await;
|
||||||
|
|
||||||
|
struct EchoHandler;
|
||||||
|
impl Handler for EchoHandler {
|
||||||
|
fn handle(
|
||||||
|
&self,
|
||||||
|
req: MqttRequest,
|
||||||
|
) -> std::pin::Pin<
|
||||||
|
Box<dyn std::future::Future<Output = Option<service::MqttResponse>> + Send>,
|
||||||
|
> {
|
||||||
|
let payload = req.payload();
|
||||||
|
Box::pin(async move { Some(service::MqttResponse::new(payload)) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mqtt_client
|
||||||
|
.add_route("test/echo", EchoHandler, QoS::AtLeastOnce)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let request = Request::new("test/echo", Bytes::from("Echo message"));
|
||||||
|
let response = mqtt_client.request(request).await.unwrap();
|
||||||
|
assert_eq!(response.get().payload, Bytes::from("Echo message"));
|
||||||
|
|
||||||
|
mqtt_client.remove_route("test/echo").await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_mqtt_client_timeout() {
|
||||||
|
let options = create_mqtt_options();
|
||||||
|
init_global_mqtt_client(options).await.unwrap();
|
||||||
|
let mqtt_client = get_global_mqtt_client().await;
|
||||||
|
|
||||||
|
let request =
|
||||||
|
Request::new("test/timeout", Bytes::from("Timeout test")).with_qos(QoS::ExactlyOnce);
|
||||||
|
let result = mqtt_client
|
||||||
|
.request_with_timeout(request, Duration::from_secs(1))
|
||||||
|
.await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
247
crates/rtsa_mqtt/src/service.rs
Normal file
247
crates/rtsa_mqtt/src/service.rs
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
future::Future,
|
||||||
|
pin::Pin,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use rumqttc::v5::mqttbytes::v5::{Publish, PublishProperties};
|
||||||
|
|
||||||
|
pub struct MqttRequest {
|
||||||
|
publish: Publish,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttRequest {
|
||||||
|
pub fn new(publish: Publish) -> Self {
|
||||||
|
MqttRequest { publish }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn topic(&self) -> String {
|
||||||
|
String::from_utf8_lossy(&self.publish.topic).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn payload(&self) -> Bytes {
|
||||||
|
self.publish.payload.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> Publish {
|
||||||
|
self.publish.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MqttResponse {
|
||||||
|
pub properties: Option<PublishProperties>,
|
||||||
|
pub payload: Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttResponse {
|
||||||
|
pub fn new(payload: Bytes) -> Self {
|
||||||
|
MqttResponse {
|
||||||
|
properties: None,
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_properties(payload: Bytes, properties: PublishProperties) -> Self {
|
||||||
|
MqttResponse {
|
||||||
|
properties: Some(properties),
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Handler: Send + Sync + 'static {
|
||||||
|
fn handle(
|
||||||
|
&self,
|
||||||
|
req: MqttRequest,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Option<MqttResponse>> + Send>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, Fut> Handler for F
|
||||||
|
where
|
||||||
|
F: Fn(MqttRequest) -> Fut + Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = Option<MqttResponse>> + Send + 'static,
|
||||||
|
{
|
||||||
|
fn handle(
|
||||||
|
&self,
|
||||||
|
req: MqttRequest,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Option<MqttResponse>> + Send>> {
|
||||||
|
Box::pin((self)(req))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MqttRouter {
|
||||||
|
routes: Arc<Mutex<HashMap<String, Arc<dyn Handler + Send + Sync>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttRouter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
MqttRouter {
|
||||||
|
routes: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_route<H>(&self, topic: &str, handler: H)
|
||||||
|
where
|
||||||
|
H: Handler + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.routes
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(topic.to_string(), Arc::new(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_route(&self, topic: &str) {
|
||||||
|
self.routes.lock().unwrap().remove(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_handler(&self, topic: &str) -> Option<Arc<dyn Handler + Send + Sync>> {
|
||||||
|
let routes = self.routes.lock().unwrap();
|
||||||
|
routes.get(topic).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&self) {
|
||||||
|
self.routes.lock().unwrap().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_request(&self, req: MqttRequest) -> Option<MqttResponse> {
|
||||||
|
if let Some(handler) = self.get_handler(&req.topic()) {
|
||||||
|
handler.handle(req).await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use rumqttc::v5::mqttbytes::v5::Publish;
|
||||||
|
|
||||||
|
// Helper function to create a Publish message
|
||||||
|
fn create_publish(topic: &str, payload: &[u8]) -> Publish {
|
||||||
|
Publish {
|
||||||
|
topic: topic.as_bytes().to_vec().into(),
|
||||||
|
payload: Bytes::from(payload.to_vec()),
|
||||||
|
dup: false,
|
||||||
|
qos: rumqttc::v5::mqttbytes::QoS::AtMostOnce,
|
||||||
|
retain: false,
|
||||||
|
pkid: 0,
|
||||||
|
properties: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample handler that echoes the payload back
|
||||||
|
async fn echo_handler(req: MqttRequest) -> Option<MqttResponse> {
|
||||||
|
Some(MqttResponse {
|
||||||
|
properties: None,
|
||||||
|
payload: req.payload(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample handler that returns None
|
||||||
|
async fn none_handler(_req: MqttRequest) -> Option<MqttResponse> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_add_and_get_handler() {
|
||||||
|
let router = MqttRouter::new();
|
||||||
|
router.add_route("test/topic", echo_handler);
|
||||||
|
|
||||||
|
assert!(router.get_handler("test/topic").is_some());
|
||||||
|
assert!(router.get_handler("invalid/topic").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_request_with_existing_route() {
|
||||||
|
let router = MqttRouter::new();
|
||||||
|
router.add_route("test/topic", echo_handler);
|
||||||
|
|
||||||
|
let publish = create_publish("test/topic", b"hello");
|
||||||
|
let req = MqttRequest::new(publish);
|
||||||
|
|
||||||
|
let response = router.handle_request(req).await;
|
||||||
|
|
||||||
|
assert!(response.is_some());
|
||||||
|
assert_eq!(response.unwrap().payload, Bytes::from("hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_request_with_nonexistent_route() {
|
||||||
|
let router = MqttRouter::new();
|
||||||
|
|
||||||
|
let publish = create_publish("invalid/topic", b"hello");
|
||||||
|
let req = MqttRequest::new(publish);
|
||||||
|
|
||||||
|
let response = router.handle_request(req).await;
|
||||||
|
|
||||||
|
assert!(response.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handler_returning_none() {
|
||||||
|
let router = MqttRouter::new();
|
||||||
|
router.add_route("test/topic", none_handler);
|
||||||
|
|
||||||
|
let publish = create_publish("test/topic", b"hello");
|
||||||
|
let req = MqttRequest::new(publish);
|
||||||
|
|
||||||
|
let response = router.handle_request(req).await;
|
||||||
|
|
||||||
|
assert!(response.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_multiple_routes() {
|
||||||
|
let router = MqttRouter::new();
|
||||||
|
router.add_route("topic/one", echo_handler);
|
||||||
|
router.add_route("topic/two", none_handler);
|
||||||
|
|
||||||
|
let publish_one = create_publish("topic/one", b"payload1");
|
||||||
|
let req_one = MqttRequest::new(publish_one);
|
||||||
|
let response_one = router.handle_request(req_one).await;
|
||||||
|
assert!(response_one.is_some());
|
||||||
|
assert_eq!(response_one.unwrap().payload, Bytes::from("payload1"));
|
||||||
|
|
||||||
|
let publish_two = create_publish("topic/two", b"payload2");
|
||||||
|
let req_two = MqttRequest::new(publish_two);
|
||||||
|
let response_two = router.handle_request(req_two).await;
|
||||||
|
assert!(response_two.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_concurrent_access() {
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Barrier;
|
||||||
|
|
||||||
|
let router = Arc::new(MqttRouter::new());
|
||||||
|
router.add_route("test/topic", echo_handler);
|
||||||
|
|
||||||
|
let barrier = Arc::new(Barrier::new(10));
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
let router_cloned = router.clone();
|
||||||
|
let barrier_cloned = barrier.clone();
|
||||||
|
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
let publish = create_publish("test/topic", b"concurrent");
|
||||||
|
let req = MqttRequest::new(publish);
|
||||||
|
|
||||||
|
barrier_cloned.wait().await;
|
||||||
|
let response = router_cloned.handle_request(req).await;
|
||||||
|
assert!(response.is_some());
|
||||||
|
assert_eq!(response.unwrap().payload, Bytes::from("concurrent"));
|
||||||
|
});
|
||||||
|
handles.push(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
for handle in handles {
|
||||||
|
handle.await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rtss_api"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = { workspace = true }
|
|
||||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
|
||||||
serde = { version = "1.0.208", features = ["derive"] }
|
|
||||||
serde_json = "1.0.125"
|
|
||||||
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.5.0", features = ["cors"] }
|
|
||||||
async-graphql = { version = "7.0.7", features = ["chrono"] }
|
|
||||||
async-graphql-axum = "7.0.6"
|
|
||||||
base64 = "0.22.1"
|
|
||||||
sysinfo = "0.31.3"
|
|
||||||
|
|
||||||
bevy_ecs = { workspace = true }
|
|
||||||
rtss_log = { path = "../rtss_log" }
|
|
||||||
rtss_sim_manage = { path = "../rtss_sim_manage" }
|
|
||||||
rtss_trackside = { path = "../rtss_trackside" }
|
|
||||||
rtss_db = { path = "../rtss_db" }
|
|
||||||
rtss_dto = { path = "../rtss_dto" }
|
|
@ -1,191 +0,0 @@
|
|||||||
use async_graphql::{Context, InputObject, Object, SimpleObject};
|
|
||||||
use chrono::{DateTime, Local};
|
|
||||||
use rtss_db::DraftDataAccessor;
|
|
||||||
use rtss_db::RtssDbAccessor;
|
|
||||||
use rtss_dto::common::DataType;
|
|
||||||
|
|
||||||
use crate::pagination::PageQueryDto;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct DraftDataQuery;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct DraftDataMutation;
|
|
||||||
|
|
||||||
#[Object]
|
|
||||||
impl DraftDataQuery {
|
|
||||||
/// 分页查询草稿数据
|
|
||||||
async fn draft_data_paging<'ctx>(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'ctx>,
|
|
||||||
paging: PageQueryDto,
|
|
||||||
query: DraftDataFilterDto,
|
|
||||||
) -> async_graphql::Result<DraftDataPage> {
|
|
||||||
let db_accessor = ctx.data::<RtssDbAccessor>()?;
|
|
||||||
let paging_result = db_accessor
|
|
||||||
.query_draft_data(query.into(), paging.into())
|
|
||||||
.await?;
|
|
||||||
Ok(paging_result.into())
|
|
||||||
}
|
|
||||||
/// 根据id获取草稿数据
|
|
||||||
async fn draft_data(&self, ctx: &Context<'_>, id: i32) -> async_graphql::Result<DraftData> {
|
|
||||||
let db_accessor = ctx.data::<RtssDbAccessor>()?;
|
|
||||||
let draft_data = db_accessor.query_draft_data_by_id(id).await?;
|
|
||||||
Ok(draft_data.into())
|
|
||||||
}
|
|
||||||
/// 查询是否已经存在同一用户下的同名草稿数据
|
|
||||||
async fn draft_data_exist(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
user_id: i32,
|
|
||||||
name: String,
|
|
||||||
) -> async_graphql::Result<bool> {
|
|
||||||
let db_accessor = ctx.data::<RtssDbAccessor>()?;
|
|
||||||
let exist = db_accessor.is_draft_data_exist(user_id, &name).await?;
|
|
||||||
Ok(exist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Object]
|
|
||||||
impl DraftDataMutation {
|
|
||||||
/// 创建草稿数据
|
|
||||||
async fn create_draft_data(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
input: CreateDraftDataDto,
|
|
||||||
) -> async_graphql::Result<DraftData> {
|
|
||||||
let db_accessor = ctx.data::<RtssDbAccessor>()?;
|
|
||||||
let draft_data = db_accessor.create_draft_data(input.into()).await?;
|
|
||||||
Ok(draft_data.into())
|
|
||||||
}
|
|
||||||
/// 更新草稿数据name
|
|
||||||
async fn update_draft_data_name(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
id: i32,
|
|
||||||
name: String,
|
|
||||||
) -> async_graphql::Result<DraftData> {
|
|
||||||
let db_accessor = ctx.data::<RtssDbAccessor>()?;
|
|
||||||
let draft_data = db_accessor.update_draft_data_name(id, &name).await?;
|
|
||||||
Ok(draft_data.into())
|
|
||||||
}
|
|
||||||
/// 更新草稿数据data
|
|
||||||
async fn update_draft_data_data(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
id: i32,
|
|
||||||
data: Vec<u8>,
|
|
||||||
) -> async_graphql::Result<DraftData> {
|
|
||||||
let db_accessor = ctx.data::<RtssDbAccessor>()?;
|
|
||||||
let draft_data = db_accessor
|
|
||||||
.update_draft_data_data(id, data.as_slice())
|
|
||||||
.await?;
|
|
||||||
Ok(draft_data.into())
|
|
||||||
}
|
|
||||||
/// 删除草稿数据
|
|
||||||
async fn delete_draft_data(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
id: Vec<i32>,
|
|
||||||
) -> async_graphql::Result<bool> {
|
|
||||||
let db_accessor = ctx.data::<RtssDbAccessor>()?;
|
|
||||||
db_accessor.delete_draft_data(id.as_slice()).await?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
/// 设置草稿数据的默认发布数据
|
|
||||||
async fn set_default_release_data_id(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
id: i32,
|
|
||||||
release_data_id: i32,
|
|
||||||
) -> async_graphql::Result<DraftData> {
|
|
||||||
let db_accessor = ctx.data::<RtssDbAccessor>()?;
|
|
||||||
let draft_data = db_accessor
|
|
||||||
.set_default_release_data_id(id, release_data_id)
|
|
||||||
.await?;
|
|
||||||
Ok(draft_data.into())
|
|
||||||
}
|
|
||||||
/// 草稿数据另存为
|
|
||||||
async fn save_as_new_draft_data(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
id: i32,
|
|
||||||
name: String,
|
|
||||||
user_id: i32,
|
|
||||||
) -> async_graphql::Result<DraftData> {
|
|
||||||
let db_accessor = ctx.data::<RtssDbAccessor>()?;
|
|
||||||
let draft_data = db_accessor.save_as_new_draft(id, &name, user_id).await?;
|
|
||||||
Ok(draft_data.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, InputObject)]
|
|
||||||
pub struct CreateDraftDataDto {
|
|
||||||
pub name: String,
|
|
||||||
pub data_type: rtss_dto::common::DataType,
|
|
||||||
pub user_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<CreateDraftDataDto> for rtss_db::CreateDraftData {
|
|
||||||
fn from(value: CreateDraftDataDto) -> Self {
|
|
||||||
Self::new(&value.name, value.data_type, value.user_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 草稿数据查询条件
|
|
||||||
#[derive(Debug, InputObject)]
|
|
||||||
pub struct DraftDataFilterDto {
|
|
||||||
pub user_id: Option<i32>,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub data_type: Option<rtss_dto::common::DataType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DraftDataFilterDto> for rtss_db::DraftDataQuery {
|
|
||||||
fn from(value: DraftDataFilterDto) -> Self {
|
|
||||||
Self {
|
|
||||||
user_id: value.user_id,
|
|
||||||
name: value.name,
|
|
||||||
data_type: value.data_type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, SimpleObject)]
|
|
||||||
pub struct DraftData {
|
|
||||||
pub id: i32,
|
|
||||||
pub name: String,
|
|
||||||
pub data_type: rtss_dto::common::DataType,
|
|
||||||
pub data: Option<Vec<u8>>,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub created_at: DateTime<Local>,
|
|
||||||
pub updated_at: DateTime<Local>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rtss_db::model::DraftDataModel> for DraftData {
|
|
||||||
fn from(value: rtss_db::model::DraftDataModel) -> Self {
|
|
||||||
Self {
|
|
||||||
id: value.id,
|
|
||||||
name: value.name,
|
|
||||||
data_type: DataType::try_from(value.data_type).unwrap(),
|
|
||||||
data: value.data,
|
|
||||||
user_id: value.user_id,
|
|
||||||
created_at: value.created_at,
|
|
||||||
updated_at: value.updated_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, SimpleObject)]
|
|
||||||
pub struct DraftDataPage {
|
|
||||||
pub total: i64,
|
|
||||||
pub data: Vec<DraftData>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<rtss_db::common::PageResult<rtss_db::model::DraftDataModel>> for DraftDataPage {
|
|
||||||
fn from(value: rtss_db::common::PageResult<rtss_db::model::DraftDataModel>) -> Self {
|
|
||||||
Self {
|
|
||||||
total: value.total,
|
|
||||||
data: value.data.into_iter().map(|m| m.into()).collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
// use std::sync::LazyLock;
|
|
||||||
|
|
||||||
// use async_graphql::Result;
|
|
||||||
// use axum::http::HeaderMap;
|
|
||||||
// use jsonwebtoken::{decode, DecodingKey, Validation};
|
|
||||||
// use rtss_log::tracing::error;
|
|
||||||
// use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
// static KEYS: LazyLock<Keys> = LazyLock::new(|| {
|
|
||||||
// // let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
|
||||||
// let secret = "joylink".to_string();
|
|
||||||
// Keys::new(secret.as_bytes())
|
|
||||||
// });
|
|
||||||
|
|
||||||
// struct Keys {
|
|
||||||
// // encoding: EncodingKey,
|
|
||||||
// decoding: DecodingKey,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl Keys {
|
|
||||||
// pub fn new(secret: &[u8]) -> Self {
|
|
||||||
// Self {
|
|
||||||
// // encoding: EncodingKey::from_secret(secret),
|
|
||||||
// decoding: DecodingKey::from_secret(secret),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[derive(Debug)]
|
|
||||||
// pub enum AuthError {
|
|
||||||
// InvalidToken,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pub(crate) fn get_token_from_headers(headers: HeaderMap) -> Result<Option<Claims>, AuthError> {
|
|
||||||
// let option_token = headers.get("Token");
|
|
||||||
// if let Some(token) = option_token {
|
|
||||||
// let token_data = decode::<Claims>(
|
|
||||||
// token.to_str().unwrap(),
|
|
||||||
// &KEYS.decoding,
|
|
||||||
// &Validation::default(),
|
|
||||||
// )
|
|
||||||
// .map_err(|err| {
|
|
||||||
// error!("Error decoding token: {:?}", err);
|
|
||||||
// AuthError::InvalidToken
|
|
||||||
// })?;
|
|
||||||
// Ok(Some(token_data.claims))
|
|
||||||
// } else {
|
|
||||||
// Ok(None)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[derive(Debug, Serialize, Deserialize)]
|
|
||||||
// pub struct Claims {
|
|
||||||
// pub id: u32,
|
|
||||||
// pub sub: String,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(test)]
|
|
||||||
// mod tests {
|
|
||||||
|
|
||||||
// use super::*;
|
|
||||||
|
|
||||||
// #[test]
|
|
||||||
// fn test_get_token_from_headers() {
|
|
||||||
// rtss_log::Logging::default().init();
|
|
||||||
// let mut headers: HeaderMap = HeaderMap::new();
|
|
||||||
// headers.insert("Token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjQ2NzAyMjcsImlkIjo2LCJvcmlnX2lhdCI6MTcyNDIzODIyNywic3ViIjoiNiJ9.sSfjdW7d3OqOE6G1p47c4dcCan4evRGoNjGPUyVfWLk".parse().unwrap());
|
|
||||||
// let result = get_token_from_headers(headers);
|
|
||||||
// match result {
|
|
||||||
// Ok(Some(claims)) => {
|
|
||||||
// assert_eq!(claims.id, 6);
|
|
||||||
// assert_eq!(claims.sub, "6");
|
|
||||||
// }
|
|
||||||
// Ok(None) => {
|
|
||||||
// panic!("Expected Some(claims), got None");
|
|
||||||
// }
|
|
||||||
// Err(e) => {
|
|
||||||
// panic!("Error: {:?}", e);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
@ -1,9 +0,0 @@
|
|||||||
mod draft_data;
|
|
||||||
// mod jwt_auth;
|
|
||||||
mod pagination;
|
|
||||||
mod server;
|
|
||||||
mod simulation;
|
|
||||||
mod simulation_definition;
|
|
||||||
mod sys_info;
|
|
||||||
|
|
||||||
pub use server::*;
|
|
@ -1,29 +0,0 @@
|
|||||||
use async_graphql::{Enum, InputObject, SimpleObject};
|
|
||||||
|
|
||||||
#[derive(Enum, Copy, Clone, Default, Eq, PartialEq, Debug)]
|
|
||||||
#[graphql(remote = "rtss_db::common::SortOrder")]
|
|
||||||
pub enum SortOrder {
|
|
||||||
#[default]
|
|
||||||
Asc,
|
|
||||||
Desc,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(InputObject, Debug)]
|
|
||||||
pub struct PageQueryDto {
|
|
||||||
pub page: i32,
|
|
||||||
pub items_per_page: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PageQueryDto> for rtss_db::common::PageQuery {
|
|
||||||
fn from(value: PageQueryDto) -> Self {
|
|
||||||
Self {
|
|
||||||
page: value.page,
|
|
||||||
items_per_page: value.items_per_page,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(SimpleObject)]
|
|
||||||
pub struct PageDto {
|
|
||||||
pub total: i64,
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
use async_graphql::{Context, InputObject, Object};
|
|
||||||
use rtss_sim_manage::{AvailablePlugins, SimulationBuilder};
|
|
||||||
|
|
||||||
use crate::simulation_definition::{MutexSimulationManager, SimulationOperation};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct SimulationQuery;
|
|
||||||
|
|
||||||
#[Object]
|
|
||||||
impl SimulationQuery {
|
|
||||||
async fn simulations<'ctx>(&self, ctx: &Context<'ctx>) -> usize {
|
|
||||||
let sim = ctx.data::<MutexSimulationManager>().unwrap();
|
|
||||||
sim.lock().await.count()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct SimulationMutation;
|
|
||||||
|
|
||||||
#[Object]
|
|
||||||
impl SimulationMutation {
|
|
||||||
async fn start_simulation<'ctx>(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'ctx>,
|
|
||||||
req: StartSimulationRequest,
|
|
||||||
) -> async_graphql::Result<String> {
|
|
||||||
// let claims = ctx.data::<Option<Claims>>().unwrap();
|
|
||||||
// match claims {
|
|
||||||
// Some(claims) => {
|
|
||||||
// info!("User {claims:?} started simulation");
|
|
||||||
// }
|
|
||||||
// _ => return Err("Unauthorized".into()),
|
|
||||||
// }
|
|
||||||
let sim = ctx.data::<MutexSimulationManager>().unwrap();
|
|
||||||
let id = sim.lock().await.start_simulation(
|
|
||||||
SimulationBuilder::default()
|
|
||||||
.id(req.user_id)
|
|
||||||
.plugins(vec![AvailablePlugins::TrackSideEquipmentPlugin]),
|
|
||||||
)?;
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn exit_simulation<'ctx>(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'ctx>,
|
|
||||||
id: String,
|
|
||||||
) -> async_graphql::Result<bool> {
|
|
||||||
let sim = ctx.data::<MutexSimulationManager>().unwrap();
|
|
||||||
sim.lock().await.exit_simulation(id)?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pause_simulation<'ctx>(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'ctx>,
|
|
||||||
id: String,
|
|
||||||
) -> async_graphql::Result<bool> {
|
|
||||||
let sim = ctx.data::<MutexSimulationManager>().unwrap();
|
|
||||||
sim.lock().await.pause_simulation(id)?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn resume_simulation<'ctx>(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'ctx>,
|
|
||||||
id: String,
|
|
||||||
) -> async_graphql::Result<bool> {
|
|
||||||
let sim = ctx.data::<MutexSimulationManager>().unwrap();
|
|
||||||
sim.lock().await.resume_simulation(id)?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_simulation_speed<'ctx>(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'ctx>,
|
|
||||||
id: String,
|
|
||||||
speed: f32,
|
|
||||||
) -> async_graphql::Result<bool> {
|
|
||||||
let sim = ctx.data::<MutexSimulationManager>().unwrap();
|
|
||||||
sim.lock().await.update_simulation_speed(id, speed)?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn trigger_simulation_operation<'ctx>(
|
|
||||||
&self,
|
|
||||||
ctx: &Context<'ctx>,
|
|
||||||
id: String,
|
|
||||||
entity_uid: String,
|
|
||||||
operation: SimulationOperation,
|
|
||||||
) -> async_graphql::Result<bool> {
|
|
||||||
let sim = ctx.data::<MutexSimulationManager>().unwrap();
|
|
||||||
sim.lock().await.trigger_entity_operation(
|
|
||||||
id,
|
|
||||||
entity_uid,
|
|
||||||
operation.to_operation_event(),
|
|
||||||
)?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(InputObject)]
|
|
||||||
struct StartSimulationRequest {
|
|
||||||
user_id: String,
|
|
||||||
sim_def_id: String,
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use async_graphql::Enum;
|
|
||||||
use bevy_ecs::event::Event;
|
|
||||||
use rtss_sim_manage::SimulationManager;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
pub struct MutexSimulationManager(Mutex<SimulationManager>);
|
|
||||||
impl Default for MutexSimulationManager {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(Mutex::new(SimulationManager::default()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Deref for MutexSimulationManager {
|
|
||||||
type Target = Mutex<SimulationManager>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)]
|
|
||||||
pub enum SimulationOperation {
|
|
||||||
TurnoutControlDC,
|
|
||||||
TurnoutControlFC,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimulationOperation {
|
|
||||||
pub fn to_operation_event(self) -> impl Event + Copy {
|
|
||||||
match self {
|
|
||||||
SimulationOperation::TurnoutControlDC => rtss_trackside::TurnoutControlEvent::DC,
|
|
||||||
SimulationOperation::TurnoutControlFC => rtss_trackside::TurnoutControlEvent::FC,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rtss_ci"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
@ -1,14 +0,0 @@
|
|||||||
pub fn add(left: u64, right: u64) -> u64 {
|
|
||||||
left + right
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rtss_common"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bevy_ecs = {workspace = true}
|
|
@ -1,76 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bevy_ecs::{component::Component, entity::Entity, system::Resource};
|
|
||||||
|
|
||||||
/// 仿真公共资源
|
|
||||||
pub struct SimulationResource {
|
|
||||||
id: String,
|
|
||||||
uid_entity_mapping: HashMap<String, Entity>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimulationResource {
|
|
||||||
pub fn new(id: String) -> Self {
|
|
||||||
SimulationResource {
|
|
||||||
id,
|
|
||||||
uid_entity_mapping: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> &str {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_entity(&self, uid: &str) -> Option<Entity> {
|
|
||||||
self.uid_entity_mapping.get(uid).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_entity(&mut self, uid: String, entity: Entity) {
|
|
||||||
self.uid_entity_mapping.insert(uid, entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备编号组件
|
|
||||||
#[derive(Component, Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct Uid(pub String);
|
|
||||||
impl Default for Uid {
|
|
||||||
fn default() -> Self {
|
|
||||||
Uid("".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
|
||||||
pub struct SharedSimulationResource(pub Arc<Mutex<SimulationResource>>);
|
|
||||||
|
|
||||||
impl SharedSimulationResource {
|
|
||||||
pub fn get_entity(&self, uid: &str) -> Option<Entity> {
|
|
||||||
self.0.lock().unwrap().uid_entity_mapping.get(uid).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_entity(&self, uid: String, entity: Entity) {
|
|
||||||
self.0
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.uid_entity_mapping
|
|
||||||
.insert(uid, entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use bevy_ecs::world;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let mut simulation_resource = SimulationResource::new("1".to_string());
|
|
||||||
let mut world = world::World::default();
|
|
||||||
let uid = Uid("1".to_string());
|
|
||||||
let entity = world.spawn(uid.clone()).id();
|
|
||||||
simulation_resource.insert_entity(uid.clone().0, entity);
|
|
||||||
assert_eq!(simulation_resource.get_entity(&uid.0), Some(entity));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rtss_db"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = { workspace = true }
|
|
||||||
sqlx = { workspace = true, features = [
|
|
||||||
"runtime-tokio",
|
|
||||||
"macros",
|
|
||||||
"chrono",
|
|
||||||
"json",
|
|
||||||
"derive",
|
|
||||||
"postgres",
|
|
||||||
"uuid",
|
|
||||||
] }
|
|
||||||
thiserror = { workspace = true }
|
|
||||||
|
|
||||||
rtss_dto = { path = "../rtss_dto" }
|
|
||||||
rtss_log = { path = "../rtss_log" }
|
|
@ -1,19 +0,0 @@
|
|||||||
mod draft_data;
|
|
||||||
pub use draft_data::*;
|
|
||||||
mod release_data;
|
|
||||||
pub use release_data::*;
|
|
||||||
|
|
||||||
pub struct RtssDbAccessor {
|
|
||||||
pool: sqlx::PgPool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtssDbAccessor {
|
|
||||||
pub fn new(pool: sqlx::PgPool) -> Self {
|
|
||||||
RtssDbAccessor { pool }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_db_accessor(url: &str) -> RtssDbAccessor {
|
|
||||||
let pool = sqlx::PgPool::connect(url).await.expect("连接数据库失败");
|
|
||||||
RtssDbAccessor::new(pool)
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum DbAccessError {
|
|
||||||
#[error("未知的数据库访问错误")]
|
|
||||||
Unknown,
|
|
||||||
#[error("sqlx 错误: {0}")]
|
|
||||||
SqlxError(#[from] sqlx::Error),
|
|
||||||
#[error("数据已存在")]
|
|
||||||
RowExist,
|
|
||||||
#[error("数据错误:{0}")]
|
|
||||||
DataError(String),
|
|
||||||
}
|
|
@ -1,306 +0,0 @@
|
|||||||
use sqlx::types::chrono::{DateTime, Local};
|
|
||||||
|
|
||||||
use crate::common::TableColumn;
|
|
||||||
|
|
||||||
/// 数据库表 rtss.draft_data 列映射
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) enum DraftDataColumn {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
Name,
|
|
||||||
DataType,
|
|
||||||
Data,
|
|
||||||
DefaultReleaseDataId,
|
|
||||||
UserId,
|
|
||||||
CreatedAt,
|
|
||||||
UpdatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
|
||||||
pub struct DraftDataModel {
|
|
||||||
pub id: i32,
|
|
||||||
pub name: String,
|
|
||||||
pub data_type: i32,
|
|
||||||
#[sqlx(default)]
|
|
||||||
pub data: Option<Vec<u8>>,
|
|
||||||
#[sqlx(default)]
|
|
||||||
pub default_release_data_id: Option<i32>,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub created_at: DateTime<Local>,
|
|
||||||
pub updated_at: DateTime<Local>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 数据库表 rtss.release_data 列映射
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) enum ReleaseDataColumn {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
Name,
|
|
||||||
DataType,
|
|
||||||
UsedVersionId,
|
|
||||||
UserId,
|
|
||||||
IsPublished,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
CreatedAt,
|
|
||||||
UpdatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
|
||||||
pub struct ReleaseDataModel {
|
|
||||||
pub id: i32,
|
|
||||||
pub name: String,
|
|
||||||
pub data_type: i32,
|
|
||||||
pub used_version_id: Option<i32>,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub is_published: bool,
|
|
||||||
pub created_at: DateTime<Local>,
|
|
||||||
pub updated_at: DateTime<Local>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 数据库表 rtss.release_data_version 列映射
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) enum ReleaseDataVersionColumn {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
ReleaseDataId,
|
|
||||||
Data,
|
|
||||||
Version,
|
|
||||||
Description,
|
|
||||||
UserId,
|
|
||||||
CreatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
|
||||||
pub struct ReleaseDataVersionModel {
|
|
||||||
pub id: i32,
|
|
||||||
pub release_data_id: i32,
|
|
||||||
#[sqlx(default)]
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
pub version: i32,
|
|
||||||
pub description: String,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub created_at: DateTime<Local>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 数据库表 rtss.feature 列映射
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) enum FeatureColumn {
|
|
||||||
Table,
|
|
||||||
Id,
|
|
||||||
FeatureType,
|
|
||||||
Name,
|
|
||||||
Description,
|
|
||||||
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,
|
|
||||||
pub is_published: bool,
|
|
||||||
pub creator_id: i32,
|
|
||||||
pub updater_id: i32,
|
|
||||||
pub created_at: DateTime<Local>,
|
|
||||||
pub updated_at: DateTime<Local>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 数据库表 rtss.feature_release_data 列映射
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) enum FeatureReleaseDataColumn {
|
|
||||||
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,
|
|
||||||
Id,
|
|
||||||
UserId,
|
|
||||||
FeatureId,
|
|
||||||
Config,
|
|
||||||
CreatedAt,
|
|
||||||
UpdatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
|
||||||
pub struct FeatureConfigModel {
|
|
||||||
pub id: i32,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub feature_id: i32,
|
|
||||||
pub config: Vec<u8>,
|
|
||||||
pub created_at: DateTime<Local>,
|
|
||||||
pub updated_at: DateTime<Local>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableColumn for DraftDataColumn {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
DraftDataColumn::Table => "rtss.draft_data",
|
|
||||||
DraftDataColumn::Id => "id",
|
|
||||||
DraftDataColumn::Name => "name",
|
|
||||||
DraftDataColumn::DataType => "data_type",
|
|
||||||
DraftDataColumn::Data => "data",
|
|
||||||
DraftDataColumn::DefaultReleaseDataId => "default_release_data_id",
|
|
||||||
DraftDataColumn::UserId => "user_id",
|
|
||||||
DraftDataColumn::CreatedAt => "created_at",
|
|
||||||
DraftDataColumn::UpdatedAt => "updated_at",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableColumn for ReleaseDataColumn {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
ReleaseDataColumn::Table => "rtss.release_data",
|
|
||||||
ReleaseDataColumn::Id => "id",
|
|
||||||
ReleaseDataColumn::Name => "name",
|
|
||||||
ReleaseDataColumn::DataType => "data_type",
|
|
||||||
ReleaseDataColumn::UsedVersionId => "used_version_id",
|
|
||||||
ReleaseDataColumn::UserId => "user_id",
|
|
||||||
ReleaseDataColumn::IsPublished => "is_published",
|
|
||||||
ReleaseDataColumn::CreatedAt => "created_at",
|
|
||||||
ReleaseDataColumn::UpdatedAt => "updated_at",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableColumn for ReleaseDataVersionColumn {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
ReleaseDataVersionColumn::Table => "rtss.release_data_version",
|
|
||||||
ReleaseDataVersionColumn::Id => "id",
|
|
||||||
ReleaseDataVersionColumn::ReleaseDataId => "release_data_id",
|
|
||||||
ReleaseDataVersionColumn::Data => "data",
|
|
||||||
ReleaseDataVersionColumn::Version => "version",
|
|
||||||
ReleaseDataVersionColumn::Description => "description",
|
|
||||||
ReleaseDataVersionColumn::UserId => "user_id",
|
|
||||||
ReleaseDataVersionColumn::CreatedAt => "created_at",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableColumn for FeatureColumn {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
FeatureColumn::Table => "rtss.feature",
|
|
||||||
FeatureColumn::Id => "id",
|
|
||||||
FeatureColumn::FeatureType => "feature_type",
|
|
||||||
FeatureColumn::Name => "name",
|
|
||||||
FeatureColumn::Description => "description",
|
|
||||||
FeatureColumn::IsPublished => "is_published",
|
|
||||||
FeatureColumn::CreatorId => "creator_id",
|
|
||||||
FeatureColumn::UpdaterId => "updater_id",
|
|
||||||
FeatureColumn::CreatedAt => "created_at",
|
|
||||||
FeatureColumn::UpdatedAt => "updated_at",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableColumn for FeatureReleaseDataColumn {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
FeatureReleaseDataColumn::Table => "rtss.feature_release_data",
|
|
||||||
FeatureReleaseDataColumn::FeatureId => "feature_id",
|
|
||||||
FeatureReleaseDataColumn::ReleaseDataId => "release_data_id",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
use std::process::Command;
|
|
||||||
|
|
||||||
use prost_build::Config;
|
|
||||||
fn main() {
|
|
||||||
// println!("cargo:rerun-if-changed=build.rs");
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
std::env::set_var(
|
|
||||||
"PROTOC",
|
|
||||||
"../../rtss-proto-msg/protoc/protoc-27.4-win64/bin/protoc.exe",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Config::new()
|
|
||||||
.out_dir("src/pb")
|
|
||||||
.type_attribute("common.DataType", "#[derive(sqlx::Type)]")
|
|
||||||
.type_attribute("common.DataType", "#[derive(async_graphql::Enum)]")
|
|
||||||
.type_attribute("common.FeatureType", "#[derive(sqlx::Type)]")
|
|
||||||
.compile_protos(
|
|
||||||
&[
|
|
||||||
"../../rtss-proto-msg/src/em_data.proto",
|
|
||||||
"../../rtss-proto-msg/src/common.proto",
|
|
||||||
],
|
|
||||||
&["../../rtss-proto-msg/src/"],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Run cargo fmt to format the generated code
|
|
||||||
Command::new("cargo")
|
|
||||||
.args(["fmt"])
|
|
||||||
.status()
|
|
||||||
.expect("Failed to run cargo fmt on rtss-dto");
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
mod pb;
|
|
||||||
|
|
||||||
pub use pb::*;
|
|
||||||
|
|
||||||
pub fn add(left: u64, right: u64) -> u64 {
|
|
||||||
left + right
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use prost::Message;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_encode_decode() {
|
|
||||||
let point = common::Point { x: 1.0, y: 2.0 };
|
|
||||||
let encoded = point.encode_to_vec();
|
|
||||||
let decoded = common::Point::decode(encoded.as_ref()).unwrap();
|
|
||||||
assert_eq!(point, decoded);
|
|
||||||
println!(
|
|
||||||
"point: {:?}, encoded: {:?}, decoded: {:?}",
|
|
||||||
point, encoded, decoded
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
/// 电子地图数据
|
|
||||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Em {
|
|
||||||
#[prost(message, optional, tag = "1")]
|
|
||||||
pub canvas: ::core::option::Option<super::common::Canvas>,
|
|
||||||
#[prost(message, repeated, tag = "2")]
|
|
||||||
pub stations: ::prost::alloc::vec::Vec<Station>,
|
|
||||||
}
|
|
||||||
/// 公里标
|
|
||||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct KilometerMark {
|
|
||||||
/// 公里标坐标系
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub coordinate: ::prost::alloc::string::String,
|
|
||||||
/// 公里标数值
|
|
||||||
#[prost(int64, tag = "2")]
|
|
||||||
pub value: i64,
|
|
||||||
}
|
|
||||||
/// 车站数据
|
|
||||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Station {
|
|
||||||
#[prost(message, optional, tag = "1")]
|
|
||||||
pub common: ::core::option::Option<super::common::CommonInfo>,
|
|
||||||
/// 车站名
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub name: ::prost::alloc::string::String,
|
|
||||||
/// 车站站名
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub zhan_name: ::prost::alloc::string::String,
|
|
||||||
/// 车站名拼音简写
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub name_pinyin: ::prost::alloc::string::String,
|
|
||||||
/// 公里标
|
|
||||||
#[prost(message, optional, tag = "6")]
|
|
||||||
pub km: ::core::option::Option<KilometerMark>,
|
|
||||||
/// 是否集中站
|
|
||||||
#[prost(bool, tag = "10")]
|
|
||||||
pub concentration: bool,
|
|
||||||
/// 是否车辆段
|
|
||||||
#[prost(bool, tag = "11")]
|
|
||||||
pub depots: bool,
|
|
||||||
/// 集中站管理的车站-id
|
|
||||||
#[prost(uint32, repeated, tag = "13")]
|
|
||||||
pub manage_station_ids: ::prost::alloc::vec::Vec<u32>,
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
pub mod common;
|
|
||||||
pub mod em_data;
|
|
@ -1,6 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rtss_iscs"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
@ -1,14 +0,0 @@
|
|||||||
pub fn add(left: u64, right: u64) -> u64 {
|
|
||||||
left + right
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rtss_sim_manage"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bevy_core = {workspace = true}
|
|
||||||
bevy_ecs = {workspace = true}
|
|
||||||
bevy_app = {workspace = true}
|
|
||||||
bevy_time = {workspace = true}
|
|
||||||
rayon = {workspace = true}
|
|
||||||
thiserror = {workspace = true}
|
|
||||||
|
|
||||||
rtss_log = { path = "../rtss_log" }
|
|
||||||
rtss_common = { path = "../rtss_common" }
|
|
||||||
rtss_trackside = { path = "../rtss_trackside" }
|
|
@ -1,17 +0,0 @@
|
|||||||
use bevy_app::App;
|
|
||||||
use rtss_trackside::TrackSideEquipmentPlugin;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum AvailablePlugins {
|
|
||||||
TrackSideEquipmentPlugin,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add_needed_plugins(app: &mut App, plugins: Vec<AvailablePlugins>) {
|
|
||||||
for plugin in plugins {
|
|
||||||
match plugin {
|
|
||||||
AvailablePlugins::TrackSideEquipmentPlugin => {
|
|
||||||
app.add_plugins(TrackSideEquipmentPlugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
mod config_plugins;
|
|
||||||
mod simulation;
|
|
||||||
pub use config_plugins::*;
|
|
||||||
pub use simulation::*;
|
|
||||||
|
|
||||||
pub fn add(left: u64, right: u64) -> u64 {
|
|
||||||
left + right
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,468 +0,0 @@
|
|||||||
use std::{
|
|
||||||
cell::RefCell,
|
|
||||||
collections::HashMap,
|
|
||||||
ops::Deref,
|
|
||||||
sync::{mpsc, Arc, Mutex},
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bevy_app::{prelude::*, PluginsState};
|
|
||||||
use bevy_ecs::{
|
|
||||||
event::{Event, EventWriter},
|
|
||||||
observer::Trigger,
|
|
||||||
system::{Query, Res, ResMut, Resource},
|
|
||||||
world::OnAdd,
|
|
||||||
};
|
|
||||||
use bevy_time::{prelude::*, TimePlugin};
|
|
||||||
use rtss_common::{SharedSimulationResource, SimulationResource, Uid};
|
|
||||||
use rtss_log::tracing::{debug, error, warn};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::{add_needed_plugins, AvailablePlugins};
|
|
||||||
|
|
||||||
/// 仿真管理器
|
|
||||||
/// 非线程安全,若需要线程安全请使用类似 `Arc<Mutex<SimulationManager>>` 的方式
|
|
||||||
pub struct SimulationManager {
|
|
||||||
txs: RefCell<HashMap<String, Simulation>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SimulationManager {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum SimulationControlError {
|
|
||||||
#[error("Unknown error")]
|
|
||||||
UnknownError,
|
|
||||||
#[error("Simulation not exist")]
|
|
||||||
SimulationNotExist,
|
|
||||||
#[error("Trigger event failed")]
|
|
||||||
TriggerEventFailed,
|
|
||||||
#[error("Simulation entity not exist")]
|
|
||||||
SimulationEntityNotExist,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimulationManager {
|
|
||||||
fn new() -> Self {
|
|
||||||
let txs = RefCell::new(HashMap::new());
|
|
||||||
SimulationManager { txs }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn count(&self) -> usize {
|
|
||||||
self.txs.borrow().len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_simulation(
|
|
||||||
&self,
|
|
||||||
builder: SimulationBuilder,
|
|
||||||
) -> Result<String, SimulationControlError> {
|
|
||||||
let id = builder.id.clone();
|
|
||||||
let sim = Simulation::new(builder);
|
|
||||||
self.txs.borrow_mut().insert(id.clone(), sim);
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exit_simulation(&self, id: String) -> Result<(), SimulationControlError> {
|
|
||||||
match self.txs.borrow_mut().remove(&id) {
|
|
||||||
Some(sim) => sim.exit_simulation(),
|
|
||||||
None => {
|
|
||||||
warn!("Simulation not exist, id={}", id);
|
|
||||||
Err(SimulationControlError::SimulationNotExist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause_simulation(&self, id: String) -> Result<(), SimulationControlError> {
|
|
||||||
match self.txs.borrow().get(&id) {
|
|
||||||
Some(sim) => sim.pause_simulation(),
|
|
||||||
None => {
|
|
||||||
warn!("Simulation not exist, id={}", id);
|
|
||||||
Err(SimulationControlError::SimulationNotExist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume_simulation(&self, id: String) -> Result<(), SimulationControlError> {
|
|
||||||
match self.txs.borrow().get(&id) {
|
|
||||||
Some(sim) => sim.resume_simulation(),
|
|
||||||
None => {
|
|
||||||
warn!("Simulation not exist, id={}", id);
|
|
||||||
Err(SimulationControlError::SimulationNotExist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_simulation_speed(
|
|
||||||
&self,
|
|
||||||
id: String,
|
|
||||||
speed: f32,
|
|
||||||
) -> Result<(), SimulationControlError> {
|
|
||||||
match self.txs.borrow().get(&id) {
|
|
||||||
Some(sim) => sim.update_simulation_speed(speed),
|
|
||||||
None => {
|
|
||||||
warn!("Simulation not exist, id={}", id);
|
|
||||||
Err(SimulationControlError::SimulationNotExist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trigger_operation<E>(&self, id: String, event: E) -> Result<(), SimulationControlError>
|
|
||||||
where
|
|
||||||
E: Event + Copy,
|
|
||||||
{
|
|
||||||
match self.txs.borrow().get(&id) {
|
|
||||||
Some(sim) => sim.trigger_operation(event),
|
|
||||||
None => {
|
|
||||||
warn!("Simulation not exist, id={}", id);
|
|
||||||
Err(SimulationControlError::SimulationNotExist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trigger_entity_operation<E>(
|
|
||||||
&self,
|
|
||||||
id: String,
|
|
||||||
entity_uid: String,
|
|
||||||
event: E,
|
|
||||||
) -> Result<(), SimulationControlError>
|
|
||||||
where
|
|
||||||
E: Event + Copy,
|
|
||||||
{
|
|
||||||
match self.txs.borrow().get(&id) {
|
|
||||||
Some(sim) => sim.trigger_entity_operation(entity_uid, event),
|
|
||||||
None => {
|
|
||||||
warn!("Simulation not exist, id={}", id);
|
|
||||||
Err(SimulationControlError::SimulationNotExist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SimulationBuilder {
|
|
||||||
/// 仿真ID
|
|
||||||
pub(crate) id: String,
|
|
||||||
/// 仿真主逻辑循环间隔,详细请查看 [`Time<Fixed>`](bevy_time::fixed::Fixed)
|
|
||||||
pub(crate) loop_duration: Duration,
|
|
||||||
/// 仿真所需插件
|
|
||||||
pub(crate) plugins: Vec<AvailablePlugins>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SimulationBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
SimulationBuilder {
|
|
||||||
id: "default".to_string(),
|
|
||||||
loop_duration: Duration::from_millis(500),
|
|
||||||
plugins: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimulationBuilder {
|
|
||||||
pub fn id(mut self, id: String) -> Self {
|
|
||||||
self.id = id;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn loop_duration(mut self, loop_duration: Duration) -> Self {
|
|
||||||
self.loop_duration = loop_duration;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn plugins(mut self, plugins: Vec<AvailablePlugins>) -> Self {
|
|
||||||
self.plugins = plugins;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource, Debug)]
|
|
||||||
pub struct SimulationId(String);
|
|
||||||
|
|
||||||
impl SimulationId {
|
|
||||||
pub fn new(id: String) -> Self {
|
|
||||||
SimulationId(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for SimulationId {
|
|
||||||
type Target = String;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource, Debug)]
|
|
||||||
pub struct SimulationStatus {
|
|
||||||
// 仿真倍速
|
|
||||||
pub speed: f32,
|
|
||||||
// 仿真是否暂停状态
|
|
||||||
pub paused: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SimulationStatus {
|
|
||||||
fn default() -> Self {
|
|
||||||
SimulationStatus {
|
|
||||||
speed: 1.0,
|
|
||||||
paused: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 仿真控制事件
|
|
||||||
#[derive(Event, Debug, Clone, Copy)]
|
|
||||||
pub enum SimulationControlEvent {
|
|
||||||
Pause,
|
|
||||||
Unpause,
|
|
||||||
UpdateSpeed(f32),
|
|
||||||
Exit,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Simulation {
|
|
||||||
tx: mpsc::Sender<Box<SimulationHandle>>,
|
|
||||||
resource: Arc<Mutex<SimulationResource>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SimulationHandle = dyn FnMut(&mut App) + Send;
|
|
||||||
|
|
||||||
impl Simulation {
|
|
||||||
pub fn new(builder: SimulationBuilder) -> Self {
|
|
||||||
let simulation_resource = Arc::new(Mutex::new(SimulationResource::new(builder.id.clone())));
|
|
||||||
let cloned_resource = Arc::clone(&simulation_resource);
|
|
||||||
|
|
||||||
let (tx, mut rx) = mpsc::channel();
|
|
||||||
|
|
||||||
rayon::spawn(move || {
|
|
||||||
let mut app = App::new();
|
|
||||||
|
|
||||||
let mut virtual_time =
|
|
||||||
Time::<Virtual>::from_max_delta(builder.loop_duration.mul_f32(2f32));
|
|
||||||
virtual_time.pause();
|
|
||||||
// 初始化仿真App
|
|
||||||
app.add_plugins(TimePlugin)
|
|
||||||
.insert_resource(virtual_time)
|
|
||||||
.insert_resource(Time::<Fixed>::from_duration(builder.loop_duration))
|
|
||||||
.insert_resource(SimulationId::new(builder.id))
|
|
||||||
.insert_resource(SimulationStatus::default())
|
|
||||||
.insert_resource(SharedSimulationResource(Arc::clone(&cloned_resource)))
|
|
||||||
.add_event::<SimulationControlEvent>()
|
|
||||||
.observe(simulation_status_control)
|
|
||||||
.observe(entity_observer);
|
|
||||||
// 添加仿真所需插件
|
|
||||||
add_needed_plugins(&mut app, builder.plugins);
|
|
||||||
|
|
||||||
let wait = Some(builder.loop_duration);
|
|
||||||
app.set_runner(move |mut app: App| {
|
|
||||||
let plugins_state = app.plugins_state();
|
|
||||||
if plugins_state != PluginsState::Cleaned {
|
|
||||||
app.finish();
|
|
||||||
app.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match runner(&mut app, wait, &mut rx) {
|
|
||||||
Ok(Some(delay)) => std::thread::sleep(delay),
|
|
||||||
Ok(None) => continue,
|
|
||||||
Err(exit) => return exit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.run();
|
|
||||||
});
|
|
||||||
Simulation {
|
|
||||||
tx,
|
|
||||||
resource: simulation_resource,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger_event(&self, event: SimulationControlEvent) -> Result<(), SimulationControlError> {
|
|
||||||
let id = self.resource.lock().unwrap().id().to_string();
|
|
||||||
let result = self.tx.send(Box::new(move |app: &mut App| {
|
|
||||||
app.world_mut().trigger(event);
|
|
||||||
}));
|
|
||||||
match result {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
"Failed to send event to simulation, id={}, error={:?}",
|
|
||||||
id, e
|
|
||||||
);
|
|
||||||
Err(SimulationControlError::TriggerEventFailed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trigger_operation<E>(&self, event: E) -> Result<(), SimulationControlError>
|
|
||||||
where
|
|
||||||
E: Event + Copy,
|
|
||||||
{
|
|
||||||
let id = self.resource.lock().unwrap().id().to_string();
|
|
||||||
let result = self.tx.send(Box::new(move |app: &mut App| {
|
|
||||||
app.world_mut().trigger(event);
|
|
||||||
}));
|
|
||||||
match result {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
"Failed to send event to simulation, id={}, error={:?}",
|
|
||||||
id, e
|
|
||||||
);
|
|
||||||
Err(SimulationControlError::TriggerEventFailed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trigger_entity_operation<E>(
|
|
||||||
&self,
|
|
||||||
entity_uid: String,
|
|
||||||
event: E,
|
|
||||||
) -> Result<(), SimulationControlError>
|
|
||||||
where
|
|
||||||
E: Event + Copy,
|
|
||||||
{
|
|
||||||
let id = self.resource.lock().unwrap().id().to_string();
|
|
||||||
match self.resource.lock().unwrap().get_entity(&entity_uid) {
|
|
||||||
Some(entity) => {
|
|
||||||
let result = self.tx.send(Box::new(move |app: &mut App| {
|
|
||||||
app.world_mut().trigger_targets(event, entity);
|
|
||||||
}));
|
|
||||||
match result {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
"Failed to send event to simulation, id={}, error={:?}",
|
|
||||||
id, e
|
|
||||||
);
|
|
||||||
Err(SimulationControlError::TriggerEventFailed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!("Entity not exist, id={}", entity_uid);
|
|
||||||
Err(SimulationControlError::SimulationEntityNotExist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exit_simulation(&self) -> Result<(), SimulationControlError> {
|
|
||||||
self.trigger_event(SimulationControlEvent::Exit)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause_simulation(&self) -> Result<(), SimulationControlError> {
|
|
||||||
self.trigger_event(SimulationControlEvent::Pause)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume_simulation(&self) -> Result<(), SimulationControlError> {
|
|
||||||
self.trigger_event(SimulationControlEvent::Unpause)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_simulation_speed(&self, speed: f32) -> Result<(), SimulationControlError> {
|
|
||||||
self.trigger_event(SimulationControlEvent::UpdateSpeed(speed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn entity_observer(
|
|
||||||
trigger: Trigger<OnAdd>,
|
|
||||||
query: Query<&Uid>,
|
|
||||||
shared: ResMut<SharedSimulationResource>,
|
|
||||||
) {
|
|
||||||
let entity = trigger.entity();
|
|
||||||
match query.get(entity) {
|
|
||||||
Ok(uid) => {
|
|
||||||
shared.insert_entity(uid.0.clone(), entity);
|
|
||||||
debug!("添加uid实体映射, Uid: {:?}, Entity: {:?}", uid, entity);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
warn!("Failed to get Uid from entity: {:?}", entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulation_status_control(
|
|
||||||
trigger: Trigger<SimulationControlEvent>,
|
|
||||||
mut time: ResMut<Time<Virtual>>,
|
|
||||||
sid: Res<SimulationId>,
|
|
||||||
mut exit: EventWriter<AppExit>,
|
|
||||||
) {
|
|
||||||
match trigger.event() {
|
|
||||||
SimulationControlEvent::Pause => {
|
|
||||||
debug!("Pausing simulation");
|
|
||||||
time.pause();
|
|
||||||
}
|
|
||||||
SimulationControlEvent::Unpause => {
|
|
||||||
debug!("Unpausing simulation");
|
|
||||||
time.unpause();
|
|
||||||
}
|
|
||||||
SimulationControlEvent::UpdateSpeed(speed) => {
|
|
||||||
debug!("Update simulation speed to {}", speed);
|
|
||||||
time.set_relative_speed(*speed);
|
|
||||||
}
|
|
||||||
SimulationControlEvent::Exit => {
|
|
||||||
debug!("Exiting simulation, id={:?}", *sid);
|
|
||||||
exit.send(AppExit::Success);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn runner(
|
|
||||||
app: &mut App,
|
|
||||||
wait: Option<Duration>,
|
|
||||||
rx: &mut mpsc::Receiver<Box<SimulationHandle>>,
|
|
||||||
) -> Result<Option<Duration>, AppExit> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
if let Err(e) = rx.try_recv().map(|mut handle| handle(app)) {
|
|
||||||
match e {
|
|
||||||
mpsc::TryRecvError::Empty => {}
|
|
||||||
mpsc::TryRecvError::Disconnected => {
|
|
||||||
error!("Simulation handle channel disconnected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.update();
|
|
||||||
|
|
||||||
if let Some(exit) = app.should_exit() {
|
|
||||||
return Err(exit);
|
|
||||||
};
|
|
||||||
|
|
||||||
let end_time = Instant::now();
|
|
||||||
|
|
||||||
if let Some(wait) = wait {
|
|
||||||
let exe_time = end_time - start_time;
|
|
||||||
if exe_time < wait {
|
|
||||||
return Ok(Some(wait - exe_time));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simulation_manager() {
|
|
||||||
let manager = SimulationManager::default();
|
|
||||||
assert_eq!(manager.count(), 0);
|
|
||||||
|
|
||||||
if let Ok(_) = manager.start_simulation(SimulationBuilder::default().id("0".to_string())) {
|
|
||||||
assert_eq!(manager.count(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(_) = manager.start_simulation(SimulationBuilder::default().id("1".to_string())) {
|
|
||||||
assert_eq!(manager.count(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(_) = manager.exit_simulation("0".to_string()) {
|
|
||||||
assert_eq!(manager.count(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(_) = manager.exit_simulation("1".to_string()) {
|
|
||||||
assert_eq!(manager.count(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rtss_trackside"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bevy_core = {workspace = true}
|
|
||||||
bevy_ecs = {workspace = true}
|
|
||||||
bevy_app = {workspace = true}
|
|
||||||
bevy_time = {workspace = true}
|
|
||||||
|
|
||||||
rtss_log = { path = "../rtss_log" }
|
|
||||||
rtss_common = { path = "../rtss_common" }
|
|
@ -1,10 +0,0 @@
|
|||||||
mod components;
|
|
||||||
mod events;
|
|
||||||
mod plugin;
|
|
||||||
mod resources;
|
|
||||||
mod systems;
|
|
||||||
pub use components::*;
|
|
||||||
pub use events::*;
|
|
||||||
pub use plugin::*;
|
|
||||||
pub use resources::*;
|
|
||||||
pub use systems::*;
|
|
8
docs/DESIGN.md
Normal file
8
docs/DESIGN.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 系统设计
|
||||||
|
|
||||||
|
## 数据库设计
|
||||||
|
- 核心数据如下图所示
|
||||||
|
|
||||||
|
![alt text](image-1.png)
|
||||||
|
|
||||||
|
- 所有数据从草稿数据开始,草稿数据发布为发布数据,功能定义引用发布数据,同时功能还可以引用其他功能实现功能目录树/功能组/功能包
|
BIN
docs/image-1.png
Normal file
BIN
docs/image-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
39
manager/Cargo.toml
Normal file
39
manager/Cargo.toml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
[package]
|
||||||
|
name = "manager"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rtsa_log = { path = "../crates/rtsa_log" }
|
||||||
|
rtsa_db = { path = "../crates/rtsa_db" }
|
||||||
|
rtsa_dto = { path = "../crates/rtsa_dto" }
|
||||||
|
|
||||||
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
config = { workspace = true }
|
||||||
|
clap = { workspace = true, features = ["derive"] }
|
||||||
|
enum_dispatch = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
axum = "0.7.5"
|
||||||
|
axum-extra = { version = "0.9.3", features = ["typed-header"] }
|
||||||
|
jsonwebtoken = "9.3.0"
|
||||||
|
tower-http = { version = "0.6.0", features = ["cors"] }
|
||||||
|
async-graphql = { version = "7.0.7", features = ["chrono", "dataloader"] }
|
||||||
|
async-graphql-axum = "7.0.6"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
sysinfo = "0.31.3"
|
||||||
|
reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls", "json"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
sqlx = { workspace = true, features = [
|
||||||
|
"runtime-tokio",
|
||||||
|
"macros",
|
||||||
|
"chrono",
|
||||||
|
"json",
|
||||||
|
"derive",
|
||||||
|
"postgres",
|
||||||
|
"uuid",
|
||||||
|
] }
|
@ -2,4 +2,4 @@
|
|||||||
url = "postgresql://joylink:Joylink@0503@10.11.11.2:5432/joylink"
|
url = "postgresql://joylink:Joylink@0503@10.11.11.2:5432/joylink"
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "info"
|
level = "debug"
|
70
manager/src/apis/data_options_def.rs
Normal file
70
manager/src/apis/data_options_def.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use async_graphql::{InputObject, InputType, OutputType, SimpleObject};
|
||||||
|
use rtsa_db::{common::TableColumn, model::DraftDataColumn};
|
||||||
|
use rtsa_dto::common::{IscsStyle, LineType};
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub trait DataOptions: InputType + OutputType + Serialize + DeserializeOwned {
|
||||||
|
fn to_data_options_filter_clause(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DataOptions for T
|
||||||
|
where
|
||||||
|
T: ?Sized + Serialize + DeserializeOwned + OutputType + InputType,
|
||||||
|
{
|
||||||
|
fn to_data_options_filter_clause(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{} @> '{}'",
|
||||||
|
DraftDataColumn::Options.name(),
|
||||||
|
serde_json::to_string(self).unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, InputObject, SimpleObject, Serialize, Deserialize)]
|
||||||
|
#[graphql(input_name = "IscsDataOptionsInput")]
|
||||||
|
pub struct IscsDataOptions {
|
||||||
|
pub style: IscsStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, InputObject, SimpleObject, Serialize, Deserialize)]
|
||||||
|
#[graphql(input_name = "LineInfoOptionsInput")]
|
||||||
|
pub struct LineInfoOptions {
|
||||||
|
pub line_type: LineType,
|
||||||
|
/// 城轨线路所属城市
|
||||||
|
pub city: Option<String>,
|
||||||
|
/// 城轨线路编号
|
||||||
|
pub line_code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_iscs_data_options_serialize() {
|
||||||
|
rtsa_log::Logging::default().init();
|
||||||
|
let options = IscsDataOptions {
|
||||||
|
style: IscsStyle::DaShiZhiNeng,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&options).unwrap();
|
||||||
|
let value = serde_json::to_value(options).unwrap();
|
||||||
|
println!("{}", json);
|
||||||
|
println!(
|
||||||
|
"serialize value: {}",
|
||||||
|
serde_json::to_string(&value).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(json, r#"{"style":"DaShiZhiNeng"}"#);
|
||||||
|
let options: IscsDataOptions = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(options.style, IscsStyle::DaShiZhiNeng);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_iscs_data_options_into_data_options_filter_clause() {
|
||||||
|
let options = IscsDataOptions {
|
||||||
|
style: IscsStyle::DaShiZhiNeng,
|
||||||
|
};
|
||||||
|
let clause = options.to_data_options_filter_clause();
|
||||||
|
println!("{clause}");
|
||||||
|
assert_eq!(clause, r#"options @> '{"style":"DaShiZhiNeng"}'"#);
|
||||||
|
}
|
||||||
|
}
|
444
manager/src/apis/draft_data.rs
Normal file
444
manager/src/apis/draft_data.rs
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
use async_graphql::dataloader::DataLoader;
|
||||||
|
use async_graphql::{ComplexObject, Context, InputObject, Object, SimpleObject};
|
||||||
|
use base64::prelude::*;
|
||||||
|
use base64::Engine;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use rtsa_db::DraftDataAccessor;
|
||||||
|
use rtsa_db::RtsaDbAccessor;
|
||||||
|
use rtsa_dto::common::{DataType, Role};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::apis::{PageDto, PageQueryDto};
|
||||||
|
use crate::loader::RtsaDbLoader;
|
||||||
|
|
||||||
|
use super::data_options_def::{DataOptions, IscsDataOptions, LineInfoOptions};
|
||||||
|
use super::release_data::ReleaseDataId;
|
||||||
|
use super::user::UserId;
|
||||||
|
|
||||||
|
use crate::user_auth::{Jwt, RoleGuard};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DraftDataQuery;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DraftDataMutation;
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl DraftDataQuery {
|
||||||
|
/// 分页查询所有草稿数据(系统管理用)
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
|
async fn draft_data_paging<'ctx>(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'ctx>,
|
||||||
|
paging: PageQueryDto,
|
||||||
|
query: DraftDataFilterDto<Value>,
|
||||||
|
) -> async_graphql::Result<PageDto<DraftDataDto>> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let paging_result = db_accessor
|
||||||
|
.paging_query_draft_data(query.into(), paging.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging_result.into())
|
||||||
|
}
|
||||||
|
/// 分页查询用户电子地图草稿数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn user_draft_em_data_paging<'ctx>(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'ctx>,
|
||||||
|
paging: PageQueryDto,
|
||||||
|
mut query: UserDraftDataFilterDto<LineInfoOptions>,
|
||||||
|
) -> async_graphql::Result<PageDto<DraftEmDataDto>> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
query = query.with_data_type_and_user_id(DataType::Em, claims.uid);
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let paging_result = db_accessor
|
||||||
|
.paging_query_draft_data(query.into(), paging.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging_result.into())
|
||||||
|
}
|
||||||
|
/// 分页查询共享的草稿电子地图数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn shared_draft_em_data_paging<'ctx>(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'ctx>,
|
||||||
|
paging: PageQueryDto,
|
||||||
|
mut query: DraftDataFilterDto<LineInfoOptions>,
|
||||||
|
) -> async_graphql::Result<PageDto<DraftEmDataDto>> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
query.data_type = Some(DataType::Em);
|
||||||
|
let paging_result = db_accessor
|
||||||
|
.paging_query_draft_data(query.into(), paging.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging_result.into())
|
||||||
|
}
|
||||||
|
/// 分页查询用户的草稿ISCS数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn user_draft_iscs_data_paging<'ctx>(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'ctx>,
|
||||||
|
paging: PageQueryDto,
|
||||||
|
mut query: UserDraftDataFilterDto<IscsDataOptions>,
|
||||||
|
) -> async_graphql::Result<PageDto<DraftIscsDataDto>> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
query = query.with_data_type_and_user_id(DataType::Iscs, claims.uid);
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let paging_result = db_accessor
|
||||||
|
.paging_query_draft_data(query.into(), paging.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging_result.into())
|
||||||
|
}
|
||||||
|
/// 分页查询共享的草稿ISCS数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn shared_draft_iscs_data_paging<'ctx>(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'ctx>,
|
||||||
|
paging: PageQueryDto,
|
||||||
|
mut query: DraftDataFilterDto<IscsDataOptions>,
|
||||||
|
) -> async_graphql::Result<PageDto<DraftIscsDataDto>> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
query.data_type = Some(DataType::Iscs);
|
||||||
|
let paging_result = db_accessor
|
||||||
|
.paging_query_draft_data(query.into(), paging.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging_result.into())
|
||||||
|
}
|
||||||
|
/// 根据id获取草稿数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn draft_data(&self, ctx: &Context<'_>, id: i32) -> async_graphql::Result<DraftDataDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let draft_data = db_accessor.query_draft_data_by_id(id).await?;
|
||||||
|
Ok(draft_data.into())
|
||||||
|
}
|
||||||
|
/// 查询是否已经存在同一用户下的同名草稿数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn draft_data_exist(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
data_type: DataType,
|
||||||
|
name: String,
|
||||||
|
) -> async_graphql::Result<bool> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
let user_id = claims.uid;
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let exist = db_accessor
|
||||||
|
.is_draft_data_exist(user_id, data_type as i32, &name)
|
||||||
|
.await?;
|
||||||
|
Ok(exist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl DraftDataMutation {
|
||||||
|
/// 创建电子地图草稿数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn create_draft_em_data(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
mut input: CreateDraftDataDto<LineInfoOptions>,
|
||||||
|
) -> async_graphql::Result<DraftDataDto> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
input = input.with_data_type_and_user_id(DataType::Em, claims.uid);
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let draft_data = db_accessor.create_draft_data(input.into()).await?;
|
||||||
|
Ok(draft_data.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建草稿ISCS数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn create_draft_iscs_data(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
mut input: CreateDraftDataDto<IscsDataOptions>,
|
||||||
|
) -> async_graphql::Result<DraftIscsDataDto> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
input = input.with_data_type_and_user_id(DataType::Iscs, claims.uid);
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let draft_data = db_accessor.create_draft_data(input.into()).await?;
|
||||||
|
Ok(draft_data.into())
|
||||||
|
}
|
||||||
|
/// 更新草稿数据name
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn update_draft_data_name(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
) -> async_graphql::Result<DraftDataDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let draft_data = db_accessor.update_draft_data_name(id, &name).await?;
|
||||||
|
Ok(draft_data.into())
|
||||||
|
}
|
||||||
|
/// 更新草稿数据data
|
||||||
|
/// data为base64编码的字符串
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn update_draft_data_data(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
data: String, // base64编码的数据
|
||||||
|
) -> async_graphql::Result<DraftDataDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let bytes = BASE64_STANDARD
|
||||||
|
.decode(data)
|
||||||
|
.map_err(|e| async_graphql::Error::new(format!("base64 decode error: {}", e)))?;
|
||||||
|
let draft_data = db_accessor.update_draft_data_data(id, &bytes).await?;
|
||||||
|
Ok(draft_data.into())
|
||||||
|
}
|
||||||
|
/// 更新草稿数据共享状态
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn update_draft_data_shared(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
is_shared: bool,
|
||||||
|
) -> async_graphql::Result<DraftDataDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let draft_data = db_accessor.set_draft_data_shared(id, is_shared).await?;
|
||||||
|
Ok(draft_data.into())
|
||||||
|
}
|
||||||
|
/// 删除草稿数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn delete_draft_data(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: Vec<i32>,
|
||||||
|
) -> async_graphql::Result<bool> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
db_accessor.delete_draft_data(id.as_slice()).await?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
/// 设置草稿数据的默认发布数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn set_default_release_data_id(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
release_data_id: i32,
|
||||||
|
) -> async_graphql::Result<DraftDataDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let draft_data = db_accessor
|
||||||
|
.set_default_release_data_id(id, release_data_id)
|
||||||
|
.await?;
|
||||||
|
Ok(draft_data.into())
|
||||||
|
}
|
||||||
|
/// 草稿数据另存为
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn save_as_new_draft_data(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
) -> async_graphql::Result<DraftDataDto> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
let user_id = claims.uid;
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let draft_data = db_accessor.save_as_new_draft(id, &name, user_id).await?;
|
||||||
|
Ok(draft_data.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, InputObject)]
|
||||||
|
#[graphql(concrete(name = "CreateDraftIscsDto", params(IscsDataOptions)))]
|
||||||
|
#[graphql(concrete(name = "CreateDraftEmDto", params(LineInfoOptions)))]
|
||||||
|
pub struct CreateDraftDataDto<T: DataOptions> {
|
||||||
|
#[graphql(skip)]
|
||||||
|
pub data_type: Option<DataType>,
|
||||||
|
pub name: String,
|
||||||
|
pub options: Option<T>,
|
||||||
|
#[graphql(skip)]
|
||||||
|
pub user_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DataOptions> CreateDraftDataDto<T> {
|
||||||
|
pub fn with_data_type_and_user_id(mut self, data_type: DataType, id: i32) -> Self {
|
||||||
|
self.data_type = Some(data_type);
|
||||||
|
self.user_id = Some(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DataOptions> From<CreateDraftDataDto<T>> for rtsa_db::CreateDraftData {
|
||||||
|
fn from(value: CreateDraftDataDto<T>) -> Self {
|
||||||
|
let cdd = Self::new(
|
||||||
|
&value.name,
|
||||||
|
value.data_type.expect("need data_type") as i32,
|
||||||
|
value.user_id.expect("CreateDraftDataDto need user_id"),
|
||||||
|
);
|
||||||
|
if value.options.is_some() {
|
||||||
|
cdd.with_options(serde_json::to_value(value.options).unwrap())
|
||||||
|
} else {
|
||||||
|
cdd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户的草稿数据查询条件
|
||||||
|
#[derive(Debug, InputObject)]
|
||||||
|
#[graphql(concrete(name = "UserDraftEmDataFilterDto", params(LineInfoOptions)))]
|
||||||
|
#[graphql(concrete(name = "UserDraftIscsDataFilterDto", params(IscsDataOptions)))]
|
||||||
|
pub struct UserDraftDataFilterDto<T: DataOptions> {
|
||||||
|
#[graphql(skip)]
|
||||||
|
pub user_id: i32,
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// 数据类型,在某个具体类型查询时不传,传了也不生效
|
||||||
|
pub data_type: Option<rtsa_dto::common::DataType>,
|
||||||
|
pub options: Option<T>,
|
||||||
|
pub is_shared: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DataOptions> UserDraftDataFilterDto<T> {
|
||||||
|
pub fn with_data_type_and_user_id(mut self, data_type: DataType, id: i32) -> Self {
|
||||||
|
self.data_type = Some(data_type);
|
||||||
|
self.user_id = id;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DataOptions> From<UserDraftDataFilterDto<T>> for rtsa_db::DraftDataQuery {
|
||||||
|
fn from(value: UserDraftDataFilterDto<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
user_id: Some(value.user_id),
|
||||||
|
name: value.name,
|
||||||
|
data_type: value.data_type.map(|dt| dt as i32),
|
||||||
|
options: value.options.map(|o| serde_json::to_value(o).unwrap()),
|
||||||
|
is_shared: value.is_shared,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 共享的草稿数据查询条件
|
||||||
|
#[derive(Debug, InputObject)]
|
||||||
|
#[graphql(concrete(name = "DraftDataFilterDto", params(Value)))]
|
||||||
|
#[graphql(concrete(name = "SharedDraftEmDataFilterDto", params(LineInfoOptions)))]
|
||||||
|
#[graphql(concrete(name = "SharedDraftIscsDataFilterDto", params(IscsDataOptions)))]
|
||||||
|
|
||||||
|
pub struct DraftDataFilterDto<T: DataOptions> {
|
||||||
|
#[graphql(skip)]
|
||||||
|
pub user_id: Option<i32>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// 数据类型,在某个具体类型查询时不传,传了也不生效
|
||||||
|
pub data_type: Option<DataType>,
|
||||||
|
pub options: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DataOptions> From<DraftDataFilterDto<T>> for rtsa_db::DraftDataQuery {
|
||||||
|
fn from(value: DraftDataFilterDto<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
user_id: value.user_id,
|
||||||
|
name: value.name,
|
||||||
|
data_type: value.data_type.map(|dt| dt as i32),
|
||||||
|
is_shared: Some(true),
|
||||||
|
options: value.options.map(|o| serde_json::to_value(o).unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
#[graphql(complex)]
|
||||||
|
pub struct DraftDataDto {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub data_type: DataType,
|
||||||
|
pub options: Option<Value>,
|
||||||
|
/// base64编码的数据
|
||||||
|
pub data: Option<String>,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub default_release_data_id: Option<i32>,
|
||||||
|
pub is_shared: bool,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ComplexObject]
|
||||||
|
impl DraftDataDto {
|
||||||
|
/// 获取默认发布数据name
|
||||||
|
async fn default_release_data_name(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
) -> async_graphql::Result<Option<String>> {
|
||||||
|
if let Some(version_id) = self.default_release_data_id {
|
||||||
|
let loader = ctx.data_unchecked::<DataLoader<RtsaDbLoader>>();
|
||||||
|
let name = loader.load_one(ReleaseDataId::new(version_id)).await?;
|
||||||
|
Ok(name)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rtsa_db::model::DraftDataModel> for DraftDataDto {
|
||||||
|
fn from(value: rtsa_db::model::DraftDataModel) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
data_type: DataType::try_from(value.data_type).unwrap(),
|
||||||
|
options: value.options,
|
||||||
|
data: value
|
||||||
|
.data
|
||||||
|
.map(|d| base64::prelude::BASE64_STANDARD.encode(d)),
|
||||||
|
user_id: value.user_id,
|
||||||
|
default_release_data_id: value.default_release_data_id,
|
||||||
|
is_shared: value.is_shared,
|
||||||
|
created_at: value.created_at.naive_local(),
|
||||||
|
updated_at: value.updated_at.naive_local(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 草稿电子地图数据
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct DraftEmDataDto {
|
||||||
|
pub draft_data: DraftDataDto,
|
||||||
|
pub options: Option<LineInfoOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rtsa_db::model::DraftDataModel> for DraftEmDataDto {
|
||||||
|
fn from(value: rtsa_db::model::DraftDataModel) -> Self {
|
||||||
|
Self {
|
||||||
|
options: value
|
||||||
|
.options
|
||||||
|
.clone()
|
||||||
|
.map(|o| serde_json::from_value(o).unwrap()),
|
||||||
|
draft_data: value.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ISCS草稿数据
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct DraftIscsDataDto {
|
||||||
|
pub draft_data: DraftDataDto,
|
||||||
|
pub options: Option<IscsDataOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rtsa_db::model::DraftDataModel> for DraftIscsDataDto {
|
||||||
|
fn from(value: rtsa_db::model::DraftDataModel) -> Self {
|
||||||
|
Self {
|
||||||
|
options: value
|
||||||
|
.options
|
||||||
|
.clone()
|
||||||
|
.map(|o| serde_json::from_value(o).unwrap()),
|
||||||
|
draft_data: value.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_base64() {
|
||||||
|
let data = b"hello world";
|
||||||
|
let encoded = BASE64_STANDARD.encode(data);
|
||||||
|
let decoded = BASE64_STANDARD.decode(&encoded).unwrap();
|
||||||
|
assert_eq!(data, decoded.as_slice());
|
||||||
|
println!("encoded: {}, decoded: {:?}", encoded, decoded);
|
||||||
|
}
|
||||||
|
}
|
225
manager/src/apis/feature.rs
Normal file
225
manager/src/apis/feature.rs
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
use crate::{
|
||||||
|
apis::{PageDto, PageQueryDto},
|
||||||
|
loader::RtsaDbLoader,
|
||||||
|
user_auth::{Jwt, RoleGuard},
|
||||||
|
};
|
||||||
|
use async_graphql::{
|
||||||
|
dataloader::DataLoader, ComplexObject, Context, InputObject, Object, SimpleObject,
|
||||||
|
};
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use rtsa_db::{CreateFeature, FeatureAccessor, RtsaDbAccessor, UpdateFeature};
|
||||||
|
use rtsa_dto::common::FeatureType;
|
||||||
|
use rtsa_dto::common::Role;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
feature_config_def::{FeatureConfig, UrFeatureConfig},
|
||||||
|
user::UserId,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FeatureQuery;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FeatureMutation;
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl FeatureQuery {
|
||||||
|
/// 分页查询功能feature(系统管理)
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
|
async fn feature_paging(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
page: PageQueryDto,
|
||||||
|
query: FeatureQueryDto,
|
||||||
|
) -> async_graphql::Result<PageDto<FeatureDto>> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let paging = dba
|
||||||
|
.paging_query_features(page.into(), &query.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// id获取功能feature
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn feature(&self, ctx: &Context<'_>, id: i32) -> async_graphql::Result<FeatureDto> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let feature = dba.get_feature(id).await?;
|
||||||
|
Ok(feature.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// id列表获取功能feature列表
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn features(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
ids: Vec<i32>,
|
||||||
|
) -> async_graphql::Result<Vec<FeatureDto>> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let features = dba.get_features(ids.as_slice()).await?;
|
||||||
|
Ok(features.into_iter().map(|f| f.into()).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl FeatureMutation {
|
||||||
|
/// 上下架功能feature
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
|
async fn publish_feature(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
is_published: bool,
|
||||||
|
) -> async_graphql::Result<FeatureDto> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let feature = dba.set_feature_published(id, is_published).await?;
|
||||||
|
Ok(feature.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建城轨仿真功能feature
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
|
async fn create_ur_feature(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
mut input: CreateFeatureDto<UrFeatureConfig>,
|
||||||
|
) -> async_graphql::Result<FeatureDto> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
input = input.with_feature_type_and_user_id(FeatureType::Ur, claims.uid);
|
||||||
|
let feature = dba.create_feature(&input.into()).await?;
|
||||||
|
Ok(feature.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新城轨仿真功能feature
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
|
async fn update_ur_feature(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
input: UpdateFeatureDto<UrFeatureConfig>,
|
||||||
|
) -> async_graphql::Result<FeatureDto> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let feature = dba.update_feature(&input.into()).await?;
|
||||||
|
Ok(feature.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, InputObject)]
|
||||||
|
#[graphql(concrete(name = "UpdateUrFeatureDto", params(UrFeatureConfig)))]
|
||||||
|
pub struct UpdateFeatureDto<T: FeatureConfig> {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub config: T,
|
||||||
|
#[graphql(skip)]
|
||||||
|
pub user_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: FeatureConfig> From<UpdateFeatureDto<T>> for UpdateFeature {
|
||||||
|
fn from(value: UpdateFeatureDto<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
description: value.description,
|
||||||
|
config: serde_json::to_value(&value.config).expect("config is to_value failed"),
|
||||||
|
updater_id: value.user_id.expect("user_id must be set"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, InputObject)]
|
||||||
|
#[graphql(concrete(name = "CreateUrFeatureDto", params(UrFeatureConfig)))]
|
||||||
|
pub struct CreateFeatureDto<T: FeatureConfig> {
|
||||||
|
#[graphql(skip)]
|
||||||
|
pub feature_type: Option<FeatureType>,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub config: T,
|
||||||
|
#[graphql(skip)]
|
||||||
|
pub user_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: FeatureConfig> From<CreateFeatureDto<T>> for CreateFeature {
|
||||||
|
fn from(value: CreateFeatureDto<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
feature_type: value.feature_type.expect("feature_type must be set"),
|
||||||
|
name: value.name,
|
||||||
|
description: value.description,
|
||||||
|
config: serde_json::to_value(&value.config).expect("config is to_value failed"),
|
||||||
|
creator_id: value.user_id.expect("user_id must be set"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: FeatureConfig> CreateFeatureDto<T> {
|
||||||
|
fn with_feature_type_and_user_id(mut self, feature_type: FeatureType, uid: i32) -> Self {
|
||||||
|
self.feature_type = Some(feature_type);
|
||||||
|
self.user_id = Some(uid);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, InputObject)]
|
||||||
|
pub struct FeatureQueryDto {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub feature_type: Option<i32>,
|
||||||
|
pub is_published: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FeatureQueryDto> for rtsa_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<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<rtsa_db::model::FeatureModel> for FeatureDto {
|
||||||
|
fn from(value: rtsa_db::model::FeatureModel) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
feature_type: FeatureType::try_from(value.feature_type).unwrap(),
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
manager/src/apis/feature_config_def.rs
Normal file
17
manager/src/apis/feature_config_def.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use async_graphql::{InputObject, InputType, OutputType, SimpleObject};
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub trait FeatureConfig: InputType + OutputType + Serialize + DeserializeOwned {}
|
||||||
|
|
||||||
|
impl FeatureConfig for Value {}
|
||||||
|
|
||||||
|
/// UR功能配置
|
||||||
|
#[derive(Debug, Clone, InputObject, SimpleObject, Serialize, Deserialize)]
|
||||||
|
#[graphql(input_name = "UrFeatureConfigInput")]
|
||||||
|
pub struct UrFeatureConfig {
|
||||||
|
/// 电子地图id
|
||||||
|
pub ems: Vec<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureConfig for UrFeatureConfig {}
|
104
manager/src/apis/mod.rs
Normal file
104
manager/src/apis/mod.rs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
use async_graphql::{Enum, InputObject, MergedObject, OutputType, SimpleObject};
|
||||||
|
use draft_data::{DraftDataMutation, DraftDataQuery};
|
||||||
|
use feature::{FeatureMutation, FeatureQuery};
|
||||||
|
use org::{OrgMutation, OrgQuery};
|
||||||
|
use release_data::{ReleaseDataMutation, ReleaseDataQuery};
|
||||||
|
|
||||||
|
mod sys_info;
|
||||||
|
use user::{UserMutation, UserQuery};
|
||||||
|
|
||||||
|
mod data_options_def;
|
||||||
|
mod draft_data;
|
||||||
|
mod feature;
|
||||||
|
mod feature_config_def;
|
||||||
|
mod org;
|
||||||
|
mod org_user;
|
||||||
|
mod release_data;
|
||||||
|
mod user;
|
||||||
|
use org_user::{OrgUserMutation, OrgUserQuery};
|
||||||
|
|
||||||
|
pub use user::UserLoginDto;
|
||||||
|
|
||||||
|
#[derive(Default, MergedObject)]
|
||||||
|
pub struct Query(
|
||||||
|
UserQuery,
|
||||||
|
OrgQuery,
|
||||||
|
OrgUserQuery,
|
||||||
|
DraftDataQuery,
|
||||||
|
ReleaseDataQuery,
|
||||||
|
FeatureQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Default, MergedObject)]
|
||||||
|
pub struct Mutation(
|
||||||
|
UserMutation,
|
||||||
|
OrgMutation,
|
||||||
|
OrgUserMutation,
|
||||||
|
DraftDataMutation,
|
||||||
|
ReleaseDataMutation,
|
||||||
|
FeatureMutation,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Enum, Copy, Clone, Default, Eq, PartialEq, Debug)]
|
||||||
|
#[graphql(remote = "rtsa_db::common::SortOrder")]
|
||||||
|
pub enum SortOrder {
|
||||||
|
#[default]
|
||||||
|
Asc,
|
||||||
|
Desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputObject, Debug)]
|
||||||
|
pub struct PageQueryDto {
|
||||||
|
#[graphql(default = 1)]
|
||||||
|
pub page: i32,
|
||||||
|
#[graphql(default = 10)]
|
||||||
|
pub items_per_page: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PageQueryDto> for rtsa_db::common::PageQuery {
|
||||||
|
fn from(value: PageQueryDto) -> Self {
|
||||||
|
Self {
|
||||||
|
page: value.page,
|
||||||
|
items_per_page: value.items_per_page,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
#[graphql(concrete(name = "UserPageDto", params(user::UserDto)))]
|
||||||
|
#[graphql(concrete(name = "DraftDataPageDto", params(draft_data::DraftDataDto)))]
|
||||||
|
#[graphql(concrete(name = "DraftEmDataPageDto", params(draft_data::DraftEmDataDto)))]
|
||||||
|
#[graphql(concrete(name = "DraftIscsDataPageDto", params(draft_data::DraftIscsDataDto)))]
|
||||||
|
#[graphql(concrete(name = "ReleaseDataPageDto", params(release_data::ReleaseDataDto)))]
|
||||||
|
#[graphql(concrete(
|
||||||
|
name = "ReleaseDataVersionPageDto",
|
||||||
|
params(release_data::ReleaseDataVersionDto)
|
||||||
|
))]
|
||||||
|
#[graphql(concrete(
|
||||||
|
name = "ReleaseEmDataPageDto",
|
||||||
|
params(release_data::ReleaseEmDataWithoutVersionDto)
|
||||||
|
))]
|
||||||
|
#[graphql(concrete(
|
||||||
|
name = "ReleaseIscsDataPageDto",
|
||||||
|
params(release_data::ReleaseIscsDataWithoutVersionDto)
|
||||||
|
))]
|
||||||
|
#[graphql(concrete(name = "FeaturePageDto", params(feature::FeatureDto)))]
|
||||||
|
pub struct PageDto<T: OutputType> {
|
||||||
|
pub total: i64,
|
||||||
|
pub items: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: OutputType> PageDto<T> {
|
||||||
|
pub fn new(total: i64, items: Vec<T>) -> Self {
|
||||||
|
Self { total, items }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: OutputType, M: Into<T>> From<rtsa_db::common::PageData<M>> for PageDto<T> {
|
||||||
|
fn from(value: rtsa_db::common::PageData<M>) -> Self {
|
||||||
|
Self::new(
|
||||||
|
value.total,
|
||||||
|
value.data.into_iter().map(|m| m.into()).collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
130
manager/src/apis/org.rs
Normal file
130
manager/src/apis/org.rs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
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_org_by_code(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
code: String,
|
||||||
|
) -> async_graphql::Result<OrgDto> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let org = dba.query_org_by_code(&code).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)
|
||||||
|
}
|
||||||
|
}
|
178
manager/src/apis/org_user.rs
Normal file
178
manager/src/apis/org_user.rs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
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 serde_json::json;
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取登录用户的组织用户信息
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn login_org_user_info(&self, ctx: &Context<'_>) -> async_graphql::Result<OrgUserDto> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
let org_user = ctx
|
||||||
|
.data::<RtsaDbAccessor>()?
|
||||||
|
.query_org_user(claims.oid, claims.uid)
|
||||||
|
.await
|
||||||
|
.or_else(|e| {
|
||||||
|
match e {
|
||||||
|
DbAccessError::RowNotExist(_) => {
|
||||||
|
// 组织用户不存在,构造组织游客角色用户
|
||||||
|
let org_user = OrganizationUserModel {
|
||||||
|
id: 0,
|
||||||
|
organization_id: claims.oid,
|
||||||
|
user_id: claims.uid,
|
||||||
|
student_id: None,
|
||||||
|
roles: json!([Role::OrgGuest]),
|
||||||
|
info: None,
|
||||||
|
creator_id: claims.uid,
|
||||||
|
created_at: chrono::Local::now(),
|
||||||
|
updater_id: claims.uid,
|
||||||
|
updated_at: chrono::Local::now(),
|
||||||
|
};
|
||||||
|
Ok(org_user)
|
||||||
|
}
|
||||||
|
_ => Err(e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
459
manager/src/apis/release_data.rs
Normal file
459
manager/src/apis/release_data.rs
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_graphql::dataloader::*;
|
||||||
|
use async_graphql::{ComplexObject, Context, InputObject, Object, SimpleObject};
|
||||||
|
use base64::prelude::*;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use rtsa_db::model::*;
|
||||||
|
use rtsa_db::prelude::*;
|
||||||
|
use rtsa_db::{model::ReleaseDataModel, ReleaseDataAccessor, RtsaDbAccessor};
|
||||||
|
use rtsa_dto::common::{DataType, Role};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::apis::draft_data::DraftDataDto;
|
||||||
|
use crate::loader::RtsaDbLoader;
|
||||||
|
|
||||||
|
use super::data_options_def::{DataOptions, IscsDataOptions, LineInfoOptions};
|
||||||
|
use super::user::UserId;
|
||||||
|
use super::{PageDto, PageQueryDto};
|
||||||
|
|
||||||
|
use crate::user_auth::{Jwt, RoleGuard};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ReleaseDataQuery;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ReleaseDataMutation;
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl ReleaseDataQuery {
|
||||||
|
/// 分页查询所有发布数据(系统管理用)
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
|
async fn release_data_paging(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
page: PageQueryDto,
|
||||||
|
query: ReleaseTypedDataFilterDto<Value>,
|
||||||
|
) -> async_graphql::Result<PageDto<ReleaseDataDto>> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let paging = db_accessor
|
||||||
|
.paging_query_release_data_list(query.into(), page.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分页查询发布的电子地图数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn release_em_data_paging(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
page: PageQueryDto,
|
||||||
|
mut query: ReleaseTypedDataFilterDto<LineInfoOptions>,
|
||||||
|
) -> async_graphql::Result<PageDto<ReleaseEmDataWithoutVersionDto>> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
query.data_type = Some(DataType::Em);
|
||||||
|
let paging = db_accessor
|
||||||
|
.paging_query_release_data_list(query.into(), page.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分页查询发布的ISCS数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn release_iscs_data_paging(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
page: PageQueryDto,
|
||||||
|
mut query: ReleaseTypedDataFilterDto<IscsDataOptions>,
|
||||||
|
) -> async_graphql::Result<PageDto<ReleaseIscsDataWithoutVersionDto>> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
query.data_type = Some(DataType::Iscs);
|
||||||
|
let paging = db_accessor
|
||||||
|
.paging_query_release_data_list(query.into(), page.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// id查询发布数据及当前使用的版本数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn release_data(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
) -> async_graphql::Result<ReleaseDataWithUsedVersionDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let model = db_accessor.query_release_data_with_used_version(id).await?;
|
||||||
|
Ok(model.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 是否已经存在相同name的发布数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn is_release_data_name_exists(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
data_type: DataType,
|
||||||
|
name: String,
|
||||||
|
) -> async_graphql::Result<bool> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let result = db_accessor
|
||||||
|
.is_release_data_name_exist(data_type as i32, &name)
|
||||||
|
.await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询发布数据的版本
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn release_data_version_paging(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
data_id: i32,
|
||||||
|
page: PageQueryDto,
|
||||||
|
) -> async_graphql::Result<PageDto<ReleaseDataVersionDto>> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let paging = db_accessor
|
||||||
|
.paging_query_release_data_version_list(data_id, page.into())
|
||||||
|
.await?;
|
||||||
|
Ok(paging.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据id获取发布数据版本详情
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn release_data_version(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
version_id: i32,
|
||||||
|
) -> async_graphql::Result<ReleaseDataVersionDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let model = db_accessor
|
||||||
|
.query_release_data_version_by_id(version_id)
|
||||||
|
.await?;
|
||||||
|
Ok(model.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl ReleaseDataMutation {
|
||||||
|
/// 发布到新的发布数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn release_new_from_draft(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
draft_id: i32,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
) -> async_graphql::Result<ReleaseDataWithUsedVersionDto> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let result = db_accessor
|
||||||
|
.release_new_from_draft(draft_id, &name, &description, Some(claims.uid))
|
||||||
|
.await?;
|
||||||
|
Ok(result.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发布到默认发布数据,需要草稿数据发布过或设置了默认发布数据id
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn release_to_default_release_data(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
draft_id: i32,
|
||||||
|
description: String,
|
||||||
|
) -> async_graphql::Result<ReleaseDataWithUsedVersionDto> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let result = db_accessor
|
||||||
|
.release_to_existing(draft_id, &description, Some(claims.uid))
|
||||||
|
.await?;
|
||||||
|
Ok(result.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新发布数据name
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn update_release_data_name(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
) -> async_graphql::Result<ReleaseDataDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let result = db_accessor.update_release_data_name(id, &name).await?;
|
||||||
|
Ok(result.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 上下架发布数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn update_release_data_published(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
is_published: bool,
|
||||||
|
) -> async_graphql::Result<ReleaseDataDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let result = db_accessor
|
||||||
|
.set_release_data_published(id, is_published)
|
||||||
|
.await?;
|
||||||
|
Ok(result.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新发布数据使用的版本
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn update_release_data_used_version(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: i32,
|
||||||
|
version_id: i32,
|
||||||
|
) -> async_graphql::Result<ReleaseDataDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let result = db_accessor
|
||||||
|
.set_release_data_used_version(id, version_id)
|
||||||
|
.await?;
|
||||||
|
Ok(result.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从发布数据版本中创建草稿数据
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn create_draft_data_from_release_data_version(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
version_id: i32,
|
||||||
|
) -> async_graphql::Result<DraftDataDto> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let result = db_accessor
|
||||||
|
.create_draft_from_release_version(version_id, claims.uid)
|
||||||
|
.await?;
|
||||||
|
Ok(result.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, InputObject)]
|
||||||
|
#[graphql(concrete(name = "ReleaseDataFilterDto", params(Value)))]
|
||||||
|
#[graphql(concrete(name = "ReleaseEmDataFilterDto", params(LineInfoOptions)))]
|
||||||
|
#[graphql(concrete(name = "ReleaseIscsDataFilterDto", params(IscsDataOptions)))]
|
||||||
|
pub struct ReleaseTypedDataFilterDto<T: DataOptions> {
|
||||||
|
#[graphql(skip)]
|
||||||
|
pub user_id: Option<i32>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// 数据类型,在某个具体类型查询时不传,传了也不生效
|
||||||
|
pub data_type: Option<DataType>,
|
||||||
|
pub options: Option<T>,
|
||||||
|
pub is_published: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DataOptions> From<ReleaseTypedDataFilterDto<T>> for rtsa_db::ReleaseDataQuery {
|
||||||
|
fn from(value: ReleaseTypedDataFilterDto<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: value.name,
|
||||||
|
user_id: value.user_id,
|
||||||
|
data_type: value.data_type.map(|dt| dt as i32),
|
||||||
|
options: value.options.map(|o| serde_json::to_value(o).unwrap()),
|
||||||
|
is_published: value.is_published,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
#[graphql(complex)]
|
||||||
|
pub struct ReleaseDataDto {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub data_type: DataType,
|
||||||
|
pub options: Option<Value>,
|
||||||
|
pub used_version_id: Option<i32>,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub is_published: bool,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ComplexObject]
|
||||||
|
impl ReleaseDataDto {
|
||||||
|
async fn description(&self, ctx: &Context<'_>) -> async_graphql::Result<Option<String>> {
|
||||||
|
if let Some(version_id) = self.used_version_id {
|
||||||
|
let loader = ctx.data_unchecked::<DataLoader<RtsaDbLoader>>();
|
||||||
|
let description = loader
|
||||||
|
.load_one(ReleaseDataVersionId::new(version_id))
|
||||||
|
.await?;
|
||||||
|
Ok(description)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
pub struct ReleaseDataId {
|
||||||
|
pub id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReleaseDataId {
|
||||||
|
pub fn new(id: i32) -> Self {
|
||||||
|
Self { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Loader<ReleaseDataId> for RtsaDbLoader {
|
||||||
|
type Value = String;
|
||||||
|
type Error = Arc<DbAccessError>;
|
||||||
|
|
||||||
|
async fn load(
|
||||||
|
&self,
|
||||||
|
keys: &[ReleaseDataId],
|
||||||
|
) -> Result<HashMap<ReleaseDataId, Self::Value>, Self::Error> {
|
||||||
|
let ids: Vec<i32> = keys.iter().map(|k| k.id).collect();
|
||||||
|
let rows = self
|
||||||
|
.db_accessor
|
||||||
|
.query_release_data_names(ids.as_slice())
|
||||||
|
.await?;
|
||||||
|
let map: HashMap<ReleaseDataId, String> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| (ReleaseDataId { id: r.0 }, r.1))
|
||||||
|
.collect();
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
pub struct ReleaseDataVersionId {
|
||||||
|
pub id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReleaseDataVersionId {
|
||||||
|
pub fn new(id: i32) -> Self {
|
||||||
|
Self { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Loader<ReleaseDataVersionId> for RtsaDbLoader {
|
||||||
|
type Value = String;
|
||||||
|
type Error = Arc<DbAccessError>;
|
||||||
|
|
||||||
|
async fn load(
|
||||||
|
&self,
|
||||||
|
keys: &[ReleaseDataVersionId],
|
||||||
|
) -> Result<HashMap<ReleaseDataVersionId, Self::Value>, Self::Error> {
|
||||||
|
let ids: Vec<i32> = keys.iter().map(|k| k.id).collect();
|
||||||
|
let rows = self
|
||||||
|
.db_accessor
|
||||||
|
.query_release_data_version_descriptions(ids.as_slice())
|
||||||
|
.await?;
|
||||||
|
let map: HashMap<ReleaseDataVersionId, String> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| (ReleaseDataVersionId { id: r.0 }, r.1))
|
||||||
|
.collect();
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发布的电子地图数据
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct ReleaseEmDataWithoutVersionDto {
|
||||||
|
pub release_data: ReleaseDataDto,
|
||||||
|
pub options: Option<LineInfoOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReleaseDataModel> for ReleaseEmDataWithoutVersionDto {
|
||||||
|
fn from(model: ReleaseDataModel) -> Self {
|
||||||
|
Self {
|
||||||
|
options: model
|
||||||
|
.options
|
||||||
|
.clone()
|
||||||
|
.map(|o| serde_json::from_value(o).unwrap()),
|
||||||
|
release_data: model.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 发布的ISCS数据
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct ReleaseIscsDataWithoutVersionDto {
|
||||||
|
pub release_data: ReleaseDataDto,
|
||||||
|
pub options: Option<IscsDataOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReleaseDataModel> for ReleaseIscsDataWithoutVersionDto {
|
||||||
|
fn from(model: ReleaseDataModel) -> Self {
|
||||||
|
Self {
|
||||||
|
options: model
|
||||||
|
.options
|
||||||
|
.clone()
|
||||||
|
.map(|o| serde_json::from_value(o).unwrap()),
|
||||||
|
release_data: model.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct ReleaseDataWithUsedVersionDto {
|
||||||
|
pub release_data: ReleaseDataDto,
|
||||||
|
pub used_version: Option<ReleaseDataVersionDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
#[graphql(complex)]
|
||||||
|
pub struct ReleaseDataVersionDto {
|
||||||
|
pub id: i32,
|
||||||
|
pub release_data_id: i32,
|
||||||
|
pub options: Option<Value>,
|
||||||
|
pub description: String,
|
||||||
|
/// base64编码的数据
|
||||||
|
pub data: String,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ComplexObject]
|
||||||
|
impl ReleaseDataVersionDto {
|
||||||
|
/// 获取用户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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(ReleaseDataModel, ReleaseDataVersionModel)> for ReleaseDataWithUsedVersionDto {
|
||||||
|
fn from((data, version): (ReleaseDataModel, ReleaseDataVersionModel)) -> Self {
|
||||||
|
Self {
|
||||||
|
release_data: data.into(),
|
||||||
|
used_version: Some(version.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReleaseDataVersionModel> for ReleaseDataVersionDto {
|
||||||
|
fn from(model: ReleaseDataVersionModel) -> Self {
|
||||||
|
Self {
|
||||||
|
id: model.id,
|
||||||
|
release_data_id: model.release_data_id,
|
||||||
|
options: model.options,
|
||||||
|
description: model.description,
|
||||||
|
data: BASE64_STANDARD.encode(model.data),
|
||||||
|
user_id: model.user_id,
|
||||||
|
created_at: model.created_at.naive_local(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReleaseDataModel> for ReleaseDataDto {
|
||||||
|
fn from(model: ReleaseDataModel) -> Self {
|
||||||
|
Self {
|
||||||
|
id: model.id,
|
||||||
|
name: model.name,
|
||||||
|
data_type: DataType::try_from(model.data_type).unwrap(),
|
||||||
|
options: model.options,
|
||||||
|
used_version_id: model.used_version_id,
|
||||||
|
user_id: model.user_id,
|
||||||
|
is_published: model.is_published,
|
||||||
|
created_at: model.created_at.naive_local(),
|
||||||
|
updated_at: model.updated_at.naive_local(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
270
manager/src/apis/user.rs
Normal file
270
manager/src/apis/user.rs
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use async_graphql::{dataloader::Loader, Context, InputObject, Object, SimpleObject};
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use rtsa_db::{
|
||||||
|
model::{OrganizationModel, OrganizationUserModel, UserModel},
|
||||||
|
DbAccessError, RtsaDbAccessor, UserAccessor,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
loader::RtsaDbLoader,
|
||||||
|
user_auth::{handle_login, Jwt, RoleGuard},
|
||||||
|
};
|
||||||
|
use rtsa_dto::common::Role;
|
||||||
|
|
||||||
|
use super::{PageDto, PageQueryDto};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct UserQuery;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct UserMutation;
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl UserQuery {
|
||||||
|
/// 用户登陆
|
||||||
|
async fn user_login(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
info: UserLoginDto,
|
||||||
|
) -> async_graphql::Result<String> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let jwt = handle_login(db_accessor, info).await?;
|
||||||
|
Ok(jwt.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户信息
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
|
async fn login_user_info(&self, ctx: &Context<'_>) -> async_graphql::Result<UserDto> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
let user = db_accessor.query_user(claims.uid).await?;
|
||||||
|
Ok(user.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户名是否存在
|
||||||
|
async fn username_exists(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
username: String,
|
||||||
|
) -> async_graphql::Result<bool> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let exist = db_accessor.is_user_name_exist(&username).await?;
|
||||||
|
Ok(exist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 邮箱是否存在
|
||||||
|
async fn email_exists(&self, ctx: &Context<'_>, email: String) -> async_graphql::Result<bool> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let exist = db_accessor.is_user_email_exist(&email).await?;
|
||||||
|
Ok(exist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 手机号是否存在
|
||||||
|
async fn mobile_exists(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
mobile: String,
|
||||||
|
) -> async_graphql::Result<bool> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let exist = db_accessor.is_user_mobile_exist(&mobile).await?;
|
||||||
|
Ok(exist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分页查询用户(系统管理)
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
|
async fn user_paging(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
page: PageQueryDto,
|
||||||
|
query: UserQueryDto,
|
||||||
|
) -> async_graphql::Result<PageDto<UserDto>> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let paging = dba.query_user_page(page.into(), query.into()).await?;
|
||||||
|
Ok(paging.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl UserMutation {
|
||||||
|
/// 用户注册
|
||||||
|
async fn register_user(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
register: RegisterUserDto,
|
||||||
|
) -> async_graphql::Result<UserDto> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let user = dba.register_user(register.into()).await?;
|
||||||
|
Ok(user.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新用户角色
|
||||||
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
|
async fn update_user_roles(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
user_id: i32,
|
||||||
|
roles: Vec<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?;
|
||||||
|
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)]
|
||||||
|
pub struct UserQueryDto {
|
||||||
|
pub id: Option<i32>,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub nickname: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub mobile: Option<String>,
|
||||||
|
pub roles: Option<Vec<Role>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UserQueryDto> for rtsa_db::UserPageFilter {
|
||||||
|
fn from(value: UserQueryDto) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
username: value.username,
|
||||||
|
nickname: value.nickname,
|
||||||
|
email: value.email,
|
||||||
|
mobile: value.mobile,
|
||||||
|
roles: value.roles.map(|r| serde_json::to_value(r).unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct LoginUserInfoDto {
|
||||||
|
pub id: i32,
|
||||||
|
pub username: String,
|
||||||
|
pub nickname: String,
|
||||||
|
pub mobile: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
/// 用户角色
|
||||||
|
pub roles: Vec<Role>,
|
||||||
|
/// 组织
|
||||||
|
pub org: LoginUserOrgInfoDto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct LoginUserOrgInfoDto {
|
||||||
|
pub org_id: i32,
|
||||||
|
pub org_code: Option<String>,
|
||||||
|
pub org_name: String,
|
||||||
|
pub roles: Vec<Role>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(UserModel, OrganizationUserModel, OrganizationModel)> for LoginUserInfoDto {
|
||||||
|
fn from(value: (UserModel, OrganizationUserModel, OrganizationModel)) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.0.id,
|
||||||
|
username: value.0.username.clone(),
|
||||||
|
nickname: value.0.nickname.clone(),
|
||||||
|
mobile: value.0.mobile.clone(),
|
||||||
|
email: value.0.email.clone(),
|
||||||
|
roles: serde_json::from_value(value.0.roles).unwrap(),
|
||||||
|
org: LoginUserOrgInfoDto {
|
||||||
|
org_id: value.2.id,
|
||||||
|
org_code: value.2.code,
|
||||||
|
org_name: value.2.name,
|
||||||
|
roles: serde_json::from_value(value.1.roles).unwrap(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct UserDto {
|
||||||
|
pub id: i32,
|
||||||
|
pub username: String,
|
||||||
|
pub nickname: String,
|
||||||
|
pub mobile: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub roles: Vec<Role>,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UserModel> for UserDto {
|
||||||
|
fn from(value: UserModel) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
username: value.username.clone(),
|
||||||
|
nickname: value.nickname.clone(),
|
||||||
|
mobile: value.mobile.clone(),
|
||||||
|
email: value.email.clone(),
|
||||||
|
roles: serde_json::from_value(value.roles).unwrap(),
|
||||||
|
created_at: value.created_at.naive_local(),
|
||||||
|
updated_at: value.updated_at.naive_local(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
pub struct UserId {
|
||||||
|
pub id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserId {
|
||||||
|
pub fn new(id: i32) -> Self {
|
||||||
|
Self { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Loader<UserId> for RtsaDbLoader {
|
||||||
|
type Value = String;
|
||||||
|
type Error = Arc<DbAccessError>;
|
||||||
|
|
||||||
|
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 rows = self
|
||||||
|
.db_accessor
|
||||||
|
.query_user_nicknames(ids.as_slice())
|
||||||
|
.await?;
|
||||||
|
let map: HashMap<UserId, String> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| (UserId::new(row.0), row.1))
|
||||||
|
.collect();
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
}
|
@ -20,9 +20,9 @@ pub struct Log {
|
|||||||
level: String,
|
level: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Log> for rtss_log::Logging {
|
impl From<Log> for rtsa_log::Logging {
|
||||||
fn from(log: Log) -> Self {
|
fn from(log: Log) -> Self {
|
||||||
rtss_log::Logging {
|
rtsa_log::Logging {
|
||||||
level: log.level.parse().unwrap(),
|
level: log.level.parse().unwrap(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@ -58,9 +58,9 @@ impl AppConfig {
|
|||||||
// Default to 'dev' env
|
// Default to 'dev' env
|
||||||
// Note that this file is _optional_
|
// Note that this file is _optional_
|
||||||
.add_source(File::with_name(&format!("{dir}/{run_mode}")).required(true))
|
.add_source(File::with_name(&format!("{dir}/{run_mode}")).required(true))
|
||||||
// Add in settings from the environment (with a prefix of RTSS_SIM)
|
// Add in settings from the environment (with a prefix of rtsa_SIM)
|
||||||
// Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key
|
// Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key
|
||||||
.add_source(Environment::with_prefix("RTSS_SIM").separator("_"))
|
.add_source(Environment::with_prefix("rtsa_SIM").separator("_"))
|
||||||
// You may also programmatically change settings
|
// You may also programmatically change settings
|
||||||
// .set_override("database.url", "postgres://")?
|
// .set_override("database.url", "postgres://")?
|
||||||
// build the configuration
|
// build the configuration
|
@ -1,10 +1,12 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use enum_dispatch::enum_dispatch;
|
use enum_dispatch::enum_dispatch;
|
||||||
|
|
||||||
use crate::{app_config, db::DbSubCommand, CmdExecutor};
|
use crate::{app_config, server, CmdExecutor};
|
||||||
|
|
||||||
|
use super::DbSubCommand;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "rtss-sim", version, author, about, long_about = None)]
|
#[command(name = "rtsa-sim", version, author, about, long_about = None)]
|
||||||
pub struct Cmd {
|
pub struct Cmd {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub cmd: SubCommand,
|
pub cmd: SubCommand,
|
||||||
@ -27,12 +29,11 @@ pub struct ServerOpts {
|
|||||||
|
|
||||||
impl CmdExecutor for ServerOpts {
|
impl CmdExecutor for ServerOpts {
|
||||||
async fn execute(&self) -> anyhow::Result<()> {
|
async fn execute(&self) -> anyhow::Result<()> {
|
||||||
println!("ServerOpts: {:?}", self);
|
|
||||||
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");
|
||||||
let log: rtss_log::Logging = app_config.log.into();
|
let log: rtsa_log::Logging = app_config.log.into();
|
||||||
log.init();
|
log.init();
|
||||||
rtss_api::serve(rtss_api::ServerConfig::new(
|
server::serve(server::ServerConfig::new(
|
||||||
&app_config.database.url,
|
&app_config.database.url,
|
||||||
app_config.server.port,
|
app_config.server.port,
|
||||||
))
|
))
|
31
manager/src/commands/db.rs
Normal file
31
manager/src/commands/db.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use enum_dispatch::enum_dispatch;
|
||||||
|
|
||||||
|
use crate::{app_config, sys_init::init_default_user_and_org, CmdExecutor};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[enum_dispatch(CmdExecutor)]
|
||||||
|
pub enum DbSubCommand {
|
||||||
|
#[command(name = "migrate", about = "Migrate database")]
|
||||||
|
Migrate(MigrateOpts),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub struct MigrateOpts {
|
||||||
|
#[clap(long, required = false, default_value = "conf")]
|
||||||
|
config_path: String,
|
||||||
|
#[clap(long, required = false, default_value = "migrations")]
|
||||||
|
file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CmdExecutor for MigrateOpts {
|
||||||
|
async fn execute(&self) -> anyhow::Result<()> {
|
||||||
|
let app_config =
|
||||||
|
app_config::AppConfig::new(&self.config_path).expect("Failed to load app config");
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
mod app_config;
|
|
||||||
mod cmd;
|
mod cmd;
|
||||||
mod db;
|
mod db;
|
||||||
|
|
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),
|
||||||
|
}
|
13
manager/src/lib.rs
Normal file
13
manager/src/lib.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
mod app_config;
|
||||||
|
mod commands;
|
||||||
|
mod sys_init;
|
||||||
|
// mod jwt_auth;
|
||||||
|
mod apis;
|
||||||
|
mod error;
|
||||||
|
mod loader;
|
||||||
|
mod server;
|
||||||
|
mod user_auth;
|
||||||
|
|
||||||
|
pub use server::*;
|
||||||
|
|
||||||
|
pub use commands::*;
|
10
manager/src/loader/mod.rs
Normal file
10
manager/src/loader/mod.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/// 数据库加载器
|
||||||
|
pub struct RtsaDbLoader {
|
||||||
|
pub(crate) db_accessor: rtsa_db::RtsaDbAccessor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RtsaDbLoader {
|
||||||
|
pub fn new(db_accessor: rtsa_db::RtsaDbAccessor) -> Self {
|
||||||
|
Self { db_accessor }
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use rtss_simulation::{Cmd, CmdExecutor};
|
use manager::{Cmd, CmdExecutor};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
@ -1,5 +1,4 @@
|
|||||||
use async_graphql::*;
|
use async_graphql::*;
|
||||||
use async_graphql::{EmptySubscription, Schema};
|
|
||||||
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
|
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
@ -9,14 +8,18 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
use dataloader::DataLoader;
|
||||||
use http::{playground_source, GraphQLPlaygroundConfig};
|
use http::{playground_source, GraphQLPlaygroundConfig};
|
||||||
use rtss_log::tracing::{debug, info};
|
use rtsa_db::RtsaDbAccessor;
|
||||||
|
use rtsa_log::tracing::{debug, info};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
use crate::draft_data;
|
use crate::apis::{Mutation, Query};
|
||||||
use crate::simulation_definition::MutexSimulationManager;
|
use crate::loader::RtsaDbLoader;
|
||||||
|
use crate::user_auth;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
@ -36,7 +39,8 @@ impl ServerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve(config: ServerConfig) -> anyhow::Result<()> {
|
pub async fn serve(config: ServerConfig) -> anyhow::Result<()> {
|
||||||
let schema = new_schema(&config).await;
|
let dba = rtsa_db::get_db_accessor(&config.database_url).await;
|
||||||
|
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))
|
||||||
@ -48,8 +52,8 @@ pub async fn serve(config: ServerConfig) -> anyhow::Result<()> {
|
|||||||
.allow_methods([Method::GET, Method::POST]),
|
.allow_methods([Method::GET, Method::POST]),
|
||||||
);
|
);
|
||||||
|
|
||||||
debug!("Server started at http://{}", config.to_socket_addr());
|
info!("Server started at http://{}", config.to_socket_addr());
|
||||||
info!("GraphiQL IDE: http://localhost:{}", config.port);
|
debug!("GraphiQL IDE: http://localhost:{}", config.port);
|
||||||
axum::serve(
|
axum::serve(
|
||||||
TcpListener::bind(config.to_socket_addr()).await.unwrap(),
|
TcpListener::bind(config.to_socket_addr()).await.unwrap(),
|
||||||
app,
|
app,
|
||||||
@ -59,21 +63,15 @@ pub async fn serve(config: ServerConfig) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn graphql_handler(
|
async fn graphql_handler(
|
||||||
State(schema): State<SimulationSchema>,
|
State(schema): State<RtsaAppSchema>,
|
||||||
_headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
req: GraphQLRequest,
|
req: GraphQLRequest,
|
||||||
) -> GraphQLResponse {
|
) -> GraphQLResponse {
|
||||||
let req = req.into_inner();
|
let mut req = req.into_inner();
|
||||||
// let mut req = req.into_inner();
|
let token = user_auth::get_token_from_headers(&headers);
|
||||||
// let token = jwt_auth::get_token_from_headers(headers);
|
if let Some(token) = token {
|
||||||
// match token {
|
req = req.data(token);
|
||||||
// Ok(token) => {
|
}
|
||||||
// req = req.data(token);
|
|
||||||
// }
|
|
||||||
// Err(e) => {
|
|
||||||
// error!("Error getting token from headers: {:?}", e);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
schema.execute(req).await.into()
|
schema.execute(req).await.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,18 +79,36 @@ async fn graphiql() -> impl IntoResponse {
|
|||||||
Html(playground_source(GraphQLPlaygroundConfig::new("/")))
|
Html(playground_source(GraphQLPlaygroundConfig::new("/")))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SimulationSchema = Schema<Query, Mutation, EmptySubscription>;
|
pub type RtsaAppSchema = Schema<Query, Mutation, EmptySubscription>;
|
||||||
|
|
||||||
#[derive(Default, MergedObject)]
|
pub struct SchemaOptions {
|
||||||
pub struct Query(draft_data::DraftDataQuery);
|
pub rtsa_dba: RtsaDbAccessor,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, MergedObject)]
|
impl SchemaOptions {
|
||||||
pub struct Mutation(draft_data::DraftDataMutation);
|
pub fn new(rtsa_dba: RtsaDbAccessor) -> Self {
|
||||||
|
Self { rtsa_dba }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn new_schema(config: &ServerConfig) -> SimulationSchema {
|
pub fn new_schema(options: SchemaOptions) -> RtsaAppSchema {
|
||||||
let dba = rtss_db::get_db_accessor(&config.database_url).await;
|
let loader = RtsaDbLoader::new(options.rtsa_dba.clone());
|
||||||
Schema::build(Query::default(), Mutation::default(), EmptySubscription)
|
Schema::build(Query::default(), Mutation::default(), EmptySubscription)
|
||||||
.data(dba)
|
.data(options.rtsa_dba)
|
||||||
.data(MutexSimulationManager::default())
|
.data(DataLoader::new(loader, tokio::spawn))
|
||||||
|
// .data(MutexSimulationManager::default())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_new_schema() {
|
||||||
|
let dba =
|
||||||
|
rtsa_db::get_db_accessor("postgresql://joylink:Joylink@0503@localhost:5432/joylink")
|
||||||
|
.await;
|
||||||
|
let _ = new_schema(SchemaOptions::new(dba));
|
||||||
|
}
|
||||||
|
}
|
84
manager/src/sys_init/mod.rs
Normal file
84
manager/src/sys_init/mod.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//! 系统初始化
|
||||||
|
|
||||||
|
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 = "4a6d74126bfd06d69406fcccb7e7d5d9";
|
||||||
|
|
||||||
|
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.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);
|
||||||
|
|
||||||
|
let user = db_accessor
|
||||||
|
.query_user_login(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(user.username, ADMIN_USER_NAME);
|
||||||
|
assert_eq!(user.nickname, ADMIN_USER_NAME);
|
||||||
|
assert_eq!(user.roles, json!([Role::Admin]));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
102
manager/src/user_auth/jwt_auth.rs
Normal file
102
manager/src/user_auth/jwt_auth.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use jsonwebtoken::{decode, DecodingKey, EncodingKey, Validation};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::error::BusinessError;
|
||||||
|
|
||||||
|
static KEYS: LazyLock<Keys> = LazyLock::new(|| {
|
||||||
|
// let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||||
|
let secret = "joylink".to_string();
|
||||||
|
Keys::new(secret.as_bytes())
|
||||||
|
});
|
||||||
|
|
||||||
|
struct Keys {
|
||||||
|
encoding: EncodingKey,
|
||||||
|
decoding: DecodingKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keys {
|
||||||
|
pub fn new(secret: &[u8]) -> Self {
|
||||||
|
Self {
|
||||||
|
encoding: EncodingKey::from_secret(secret),
|
||||||
|
decoding: DecodingKey::from_secret(secret),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
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)]
|
||||||
|
pub struct Claims {
|
||||||
|
/// 用户ID
|
||||||
|
pub uid: i32,
|
||||||
|
/// 组织ID
|
||||||
|
pub oid: i32,
|
||||||
|
exp: usize, // 过期时间,单位秒
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_timestamp() -> u64 {
|
||||||
|
let start = std::time::SystemTime::now();
|
||||||
|
start
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards")
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXP_TIME: usize = 3600 * 24 * 5; // 5天
|
||||||
|
|
||||||
|
impl Claims {
|
||||||
|
pub fn new(uid: i32, oid: i32) -> Self {
|
||||||
|
Self {
|
||||||
|
uid,
|
||||||
|
oid,
|
||||||
|
exp: get_current_timestamp() as usize + EXP_TIME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jwt() {
|
||||||
|
rtsa_log::Logging::default().init();
|
||||||
|
let claim = Claims::new(5, 1);
|
||||||
|
let jwt = Jwt::build_from(claim).unwrap();
|
||||||
|
println!("jwt: {}", jwt.0);
|
||||||
|
let result = jwt.decode();
|
||||||
|
match result {
|
||||||
|
Ok(claims) => {
|
||||||
|
assert_eq!(claims.uid, 5);
|
||||||
|
assert_eq!(claims.oid, 1);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
panic!("Error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
223
manager/src/user_auth/mod.rs
Normal file
223
manager/src/user_auth/mod.rs
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
use async_graphql::Guard;
|
||||||
|
use rtsa_db::prelude::*;
|
||||||
|
use rtsa_dto::common::Role;
|
||||||
|
use rtsa_log::tracing::{error, info, warn};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
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,
|
||||||
|
user_login_dto: UserLoginDto,
|
||||||
|
) -> Result<Jwt, BusinessError> {
|
||||||
|
let org_id;
|
||||||
|
if let Some(oid) = user_login_dto.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(&user_login_dto.username, &user_login_dto.password)
|
||||||
|
.await;
|
||||||
|
match user {
|
||||||
|
Ok(user) => {
|
||||||
|
// 用户存在
|
||||||
|
Ok(Jwt::build_from(Claims::new(user.id, org_id))?)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
match e {
|
||||||
|
DbAccessError::UserNotExist(e) => {
|
||||||
|
info!(
|
||||||
|
"用户不存在: {}, 尝试查询组织学工号用户: username={}, org_id={}",
|
||||||
|
e, user_login_dto.username, org_id
|
||||||
|
);
|
||||||
|
// 用户不存在,查询组织用户学工号+用户密码
|
||||||
|
// 通过组织id和学工号查询组织用户
|
||||||
|
let org_user = db_accessor
|
||||||
|
.query_org_user_by_student_id(org_id, &user_login_dto.username)
|
||||||
|
.await;
|
||||||
|
if org_user.is_err() {
|
||||||
|
warn!(
|
||||||
|
"用户不存在: username={}, org_id={}",
|
||||||
|
user_login_dto.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(
|
||||||
|
&user_login_dto.password,
|
||||||
|
&user.password,
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
Ok(Jwt::build_from(Claims::new(user.id, org_id))?)
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"密码不匹配: username={}, org_id={}",
|
||||||
|
user_login_dto.username, org_id
|
||||||
|
);
|
||||||
|
Err(BusinessError::AuthError(
|
||||||
|
"用户不存在或密码不正确".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DbAccessError::PasswordNotMatch => {
|
||||||
|
warn!(
|
||||||
|
"密码不匹配: username={}, password={}, org_id={}",
|
||||||
|
user_login_dto.username, user_login_dto.password, org_id
|
||||||
|
);
|
||||||
|
Err(BusinessError::AuthError(
|
||||||
|
"用户不存在或密码不正确".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!(
|
||||||
|
"查询用户登录验证错误: username={}, org_id={}, error={:?}",
|
||||||
|
user_login_dto.username, org_id, e
|
||||||
|
);
|
||||||
|
Err(BusinessError::AuthError(
|
||||||
|
"用户不存在或密码不正确".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询登录用户、组织等信息
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) async fn query_login_user_info(
|
||||||
|
db_accessor: &RtsaDbAccessor,
|
||||||
|
claims: Claims,
|
||||||
|
) -> Result<(UserModel, OrganizationUserModel, OrganizationModel), BusinessError> {
|
||||||
|
let user = db_accessor.query_user(claims.uid).await?;
|
||||||
|
let org = db_accessor.query_org(claims.oid).await?;
|
||||||
|
let org_user = db_accessor.query_org_user(claims.oid, claims.uid).await;
|
||||||
|
match org_user {
|
||||||
|
Ok(org_user) => Ok((user, org_user, org)),
|
||||||
|
Err(_) => {
|
||||||
|
// 组织用户不存在,构造组织游客角色用户
|
||||||
|
let org_user = OrganizationUserModel {
|
||||||
|
id: 0,
|
||||||
|
organization_id: claims.oid,
|
||||||
|
user_id: claims.uid,
|
||||||
|
student_id: None,
|
||||||
|
roles: json!([Role::OrgGuest]),
|
||||||
|
info: None,
|
||||||
|
creator_id: 0,
|
||||||
|
created_at: chrono::Local::now(),
|
||||||
|
updater_id: 0,
|
||||||
|
updated_at: chrono::Local::now(),
|
||||||
|
};
|
||||||
|
Ok((user, org_user, org))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "rtsa_db::MIGRATOR")]
|
||||||
|
async fn test_query_login_user_info(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();
|
||||||
|
let claims = jwt.decode().unwrap();
|
||||||
|
let (user, org_user, org) = query_login_user_info(&accessor, claims).await.unwrap();
|
||||||
|
assert_eq!(user.username, ADMIN_USER_NAME);
|
||||||
|
assert_eq!(org_user.organization_id, org.id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
DROP SCHEMA rtss CASCADE;
|
DROP SCHEMA rtsa CASCADE;
|
||||||
|
@ -1,234 +1,651 @@
|
|||||||
-- 初始化数据库SCHEMA(所有轨道交通信号系统仿真的表、类型等都在rtss SCHEMA下)
|
-- 初始化数据库SCHEMA(所有轨道交通信号系统仿真的表、类型等都在rtsa SCHEMA下)
|
||||||
CREATE SCHEMA rtss;
|
CREATE SCHEMA rtsa;
|
||||||
|
|
||||||
-- 创建草稿数据表
|
-- 创建mqtt客户端id序列
|
||||||
|
CREATE SEQUENCE rtsa.mqtt_client_id_seq;
|
||||||
|
|
||||||
|
-- 创建用户表
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtss.draft_data (
|
rtsa.user (
|
||||||
id SERIAL PRIMARY KEY, -- id 自增主键
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
name VARCHAR(128) NOT NULL, -- 草稿名称
|
username VARCHAR(128) NOT NULL UNIQUE, -- 用户名
|
||||||
|
password VARCHAR(256) NOT NULL, -- 密码
|
||||||
|
nickname VARCHAR(128) NOT NULL DEFAULT 'user', -- 昵称
|
||||||
|
email VARCHAR(128) NULL UNIQUE, -- 邮箱
|
||||||
|
mobile VARCHAR(16) NULL UNIQUE, -- 手机号
|
||||||
|
roles JSONB NOT NULL DEFAULT '[]', -- 角色列表
|
||||||
|
info JSONB NOT NULL DEFAULT '{}', -- 用户信息
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP -- 更新时间
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建用户名称索引
|
||||||
|
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 (mobile);
|
||||||
|
|
||||||
|
-- 创建用户角色索引
|
||||||
|
CREATE INDEX ON rtsa.user USING GIN (roles);
|
||||||
|
|
||||||
|
-- 注释用户表
|
||||||
|
COMMENT ON TABLE rtsa.user IS '用户表';
|
||||||
|
|
||||||
|
-- 注释用户表字段
|
||||||
|
COMMENT ON COLUMN rtsa.user.id IS 'id 自增主键';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.username IS '用户名';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.password IS '密码';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.nickname IS '昵称';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.email IS '邮箱';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.mobile IS '手机号';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.roles IS '角色列表';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.info IS '用户信息';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.created_at IS '创建时间';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 创建组织表
|
||||||
|
CREATE TABLE
|
||||||
|
rtsa.organization (
|
||||||
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
|
code VARCHAR(128) NULL UNIQUE, -- 组织编码
|
||||||
|
name VARCHAR(128) NOT NULL, -- 组织名称
|
||||||
|
config JSONB NULL, -- 配置数据
|
||||||
|
parent_id INT NULL, -- 父组织id
|
||||||
|
creator_id INT NOT NULL, -- 创建用户id
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
|
updater_id INT NOT NULL, -- 更新用户id
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
|
||||||
|
FOREIGN KEY (parent_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 (code);
|
||||||
|
|
||||||
|
-- 创建组织名称索引
|
||||||
|
CREATE INDEX ON rtsa.organization (name);
|
||||||
|
|
||||||
|
-- 创建组织父组织索引
|
||||||
|
CREATE INDEX ON rtsa.organization (parent_id);
|
||||||
|
|
||||||
|
-- 创建组织创建用户索引
|
||||||
|
CREATE INDEX ON rtsa.organization (creator_id);
|
||||||
|
|
||||||
|
-- 创建组织更新用户索引
|
||||||
|
CREATE INDEX ON rtsa.organization (updater_id);
|
||||||
|
|
||||||
|
-- 注释组织表
|
||||||
|
COMMENT ON TABLE rtsa.organization IS '组织表';
|
||||||
|
|
||||||
|
-- 注释组织表字段
|
||||||
|
COMMENT ON COLUMN rtsa.organization.id IS 'id 自增主键';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization.code IS '组织编码';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization.name IS '组织名称';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization.config IS '配置数据';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization.parent_id IS '父组织id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization.creator_id IS '创建用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization.created_at IS '创建时间';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization.updater_id IS '更新用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 创建组织用户表
|
||||||
|
CREATE TABLE
|
||||||
|
rtsa.organization_user (
|
||||||
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
|
organization_id INT NOT NULL, -- 组织id
|
||||||
|
user_id INT NOT NULL, -- 用户id
|
||||||
|
student_id VARCHAR(32) NULL, -- 学工号
|
||||||
|
roles JSONB NOT NULL DEFAULT '[]', -- 组织角色列表
|
||||||
|
info JSONB NULL, -- 组织用户额外信息
|
||||||
|
creator_id INT NOT NULL, -- 创建用户id
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
|
updater_id INT NOT NULL, -- 更新用户id
|
||||||
|
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 (creator_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 创建用户外键
|
||||||
|
FOREIGN KEY (updater_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 更新用户外键
|
||||||
|
UNIQUE(organization_id, student_id), -- 组织id+学工号唯一
|
||||||
|
UNIQUE(organization_id, user_id) -- 组织id+用户id唯一
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建组织用户组织索引
|
||||||
|
CREATE INDEX ON rtsa.organization_user (organization_id);
|
||||||
|
|
||||||
|
-- 创建组织用户用户索引
|
||||||
|
CREATE INDEX ON rtsa.organization_user (user_id);
|
||||||
|
|
||||||
|
-- 创建组织用户学工号索引
|
||||||
|
CREATE INDEX ON rtsa.organization_user (student_id);
|
||||||
|
|
||||||
|
-- 创建组织用户角色索引
|
||||||
|
CREATE INDEX ON rtsa.organization_user USING GIN (roles);
|
||||||
|
|
||||||
|
-- 创建组织用户创建用户索引
|
||||||
|
CREATE INDEX ON rtsa.organization_user (creator_id);
|
||||||
|
|
||||||
|
-- 创建组织用户更新用户索引
|
||||||
|
CREATE INDEX ON rtsa.organization_user (updater_id);
|
||||||
|
|
||||||
|
-- 注释组织用户表
|
||||||
|
COMMENT ON TABLE rtsa.organization_user IS '组织用户表';
|
||||||
|
|
||||||
|
-- 注释组织用户表字段
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.id IS 'id 自增主键';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.organization_id IS '组织id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.user_id IS '用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.student_id IS '学工号';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.roles IS '组织角色列表';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.info IS '组织用户其他信息';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.creator_id IS '创建用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.created_at IS '创建时间';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.updater_id IS '更新用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_user.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 创建组织数据表
|
||||||
|
CREATE TABLE
|
||||||
|
rtsa.organization_data (
|
||||||
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
|
organization_id INT NOT NULL, -- 组织id
|
||||||
data_type INT NOT NULL, -- 数据类型
|
data_type INT NOT NULL, -- 数据类型
|
||||||
data BYTEA, -- 草稿数据
|
data JSONB NOT NULL, -- 数据
|
||||||
default_release_data_id INT NULL, -- 默认发布数据id
|
creator_id INT NOT NULL, -- 创建用户id
|
||||||
user_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 'now()', -- 创建时间
|
updater_id INT NOT NULL, -- 更新用户id
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()', -- 更新时间
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
|
||||||
UNIQUE (name, user_id) -- 一个用户的草稿名称唯一
|
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 rtss.draft_data (user_id);
|
CREATE INDEX ON rtsa.organization_data (organization_id);
|
||||||
|
|
||||||
-- 创建草稿数据类型索引
|
-- 创建组织数据类型索引
|
||||||
CREATE INDEX ON rtss.draft_data (data_type);
|
CREATE INDEX ON rtsa.organization_data (data_type);
|
||||||
|
|
||||||
-- 注释草稿数据表
|
-- 创建组织数据配置项索引
|
||||||
COMMENT ON TABLE rtss.draft_data IS '草稿数据表';
|
CREATE INDEX ON rtsa.organization_data USING GIN (data);
|
||||||
|
|
||||||
-- 注释草稿数据表字段
|
-- 创建组织数据创建用户索引
|
||||||
COMMENT ON COLUMN rtss.draft_data.id IS 'id 自增主键';
|
CREATE INDEX ON rtsa.organization_data (creator_id);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.draft_data.name IS '草稿名称';
|
-- 创建组织数据更新用户索引
|
||||||
|
CREATE INDEX ON rtsa.organization_data (updater_id);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.draft_data.data_type IS '数据类型';
|
-- 创建组织数据索引
|
||||||
|
CREATE INDEX ON rtsa.organization_data (organization_id, data_type);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.draft_data.data IS '草稿数据';
|
-- 注释组织数据表
|
||||||
|
COMMENT ON TABLE rtsa.organization_data IS '组织数据表';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.draft_data.user_id IS '创建用户id';
|
-- 注释组织数据表字段
|
||||||
|
COMMENT ON COLUMN rtsa.organization_data.id IS 'id 自增主键';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.draft_data.created_at IS '创建时间';
|
COMMENT ON COLUMN rtsa.organization_data.organization_id IS '组织id';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.draft_data.updated_at IS '更新时间';
|
COMMENT ON COLUMN rtsa.organization_data.data_type IS '数据类型';
|
||||||
|
|
||||||
-- 创建发布数据表
|
COMMENT ON COLUMN rtsa.organization_data.data IS '数据';
|
||||||
CREATE TABLE
|
|
||||||
rtss.release_data (
|
|
||||||
id SERIAL PRIMARY KEY, -- id 自增主键
|
|
||||||
name VARCHAR(128) NOT NULL UNIQUE, -- 发布数据名称(数据唯一标识)
|
|
||||||
data_type INT NOT NULL, -- 数据类型
|
|
||||||
used_version_id INT NULL, -- 使用的版本数据id
|
|
||||||
user_id INT NOT NULL, -- 发布/更新用户id
|
|
||||||
is_published BOOLEAN NOT NULL DEFAULT TRUE, -- 是否上架
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()', -- 创建时间
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()' -- 更新时间
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 注释发布数据表
|
COMMENT ON COLUMN rtsa.organization_data.creator_id IS '创建用户id';
|
||||||
COMMENT ON TABLE rtss.release_data IS '发布数据表';
|
|
||||||
|
|
||||||
-- 注释发布数据表字段
|
COMMENT ON COLUMN rtsa.organization_data.created_at IS '创建时间';
|
||||||
COMMENT ON COLUMN rtss.release_data.id IS 'id 自增主键';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data.name IS '发布数据名称(数据唯一标识)';
|
COMMENT ON COLUMN rtsa.organization_data.updater_id IS '更新用户id';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data.data_type IS '数据类型';
|
COMMENT ON COLUMN rtsa.organization_data.updated_at IS '更新时间';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data.used_version_id IS '使用的版本数据id';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data.user_id IS '发布/更新用户id';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data.is_published IS '是否上架';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data.created_at IS '创建时间';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data.updated_at IS '更新时间';
|
|
||||||
|
|
||||||
-- 创建发布数据版本表
|
|
||||||
CREATE TABLE
|
|
||||||
rtss.release_data_version (
|
|
||||||
id SERIAL PRIMARY KEY, -- id 自增主键
|
|
||||||
release_data_id INT NOT NULL, -- 发布数据id
|
|
||||||
data BYTEA NOT NULL, -- 数据
|
|
||||||
version SERIAL NOT NULL, -- 版本号
|
|
||||||
description TEXT NOT NULL, -- 版本描述
|
|
||||||
user_id INT NOT NULL, -- 发布用户id
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()', -- 创建时间
|
|
||||||
FOREIGN KEY (release_data_id) REFERENCES rtss.release_data (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 创建发布数据当前版本外键
|
|
||||||
ALTER TABLE rtss.release_data ADD FOREIGN KEY (used_version_id) REFERENCES rtss.release_data_version (id) ON DELETE SET NULL;
|
|
||||||
|
|
||||||
-- 创建草稿数据默认发布数据外键
|
|
||||||
ALTER TABLE rtss.draft_data ADD FOREIGN KEY (default_release_data_id) REFERENCES rtss.release_data (id) ON DELETE SET NULL;
|
|
||||||
|
|
||||||
-- 注释发布数据版本表
|
|
||||||
COMMENT ON TABLE rtss.release_data_version IS '发布数据版本表';
|
|
||||||
|
|
||||||
-- 注释发布数据版本表字段
|
|
||||||
COMMENT ON COLUMN rtss.release_data_version.id IS 'id 自增主键';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data_version.release_data_id IS '发布数据id';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data_version.data IS '数据';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data_version.version IS '版本号';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data_version.description IS '版本描述';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data_version.user_id IS '发布用户id';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.release_data_version.created_at IS '创建时间';
|
|
||||||
|
|
||||||
-- 创建feature表
|
-- 创建feature表
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtss.feature (
|
rtsa.feature (
|
||||||
id SERIAL PRIMARY KEY, -- id 自增主键
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
feature_type INT NOT NULL, -- feature类型
|
feature_type INT NOT NULL, -- feature类型
|
||||||
name VARCHAR(128) NOT NULL UNIQUE, -- feature名称
|
name VARCHAR(128) NOT NULL, -- 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
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()', -- 创建时间
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()' -- 更新时间
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
|
||||||
|
FOREIGN KEY (creator_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 用户外键
|
||||||
|
FOREIGN KEY (updater_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 用户外键
|
||||||
|
UNIQUE(feature_type, name) -- feature类型和名称唯一
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 创建feature类型索引
|
||||||
|
CREATE INDEX ON rtsa.feature (feature_type);
|
||||||
|
|
||||||
|
-- 创建feature名称索引
|
||||||
|
CREATE INDEX ON rtsa.feature (name);
|
||||||
|
|
||||||
-- 注释仿真feature表
|
-- 注释仿真feature表
|
||||||
COMMENT ON TABLE rtss.feature IS 'feature表';
|
COMMENT ON TABLE rtsa.feature IS 'feature表';
|
||||||
|
|
||||||
-- 注释仿真feature表字段
|
-- 注释仿真feature表字段
|
||||||
COMMENT ON COLUMN rtss.feature.id IS 'id 自增主键';
|
COMMENT ON COLUMN rtsa.feature.id IS 'id 自增主键';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature.feature_type IS 'feature类型';
|
COMMENT ON COLUMN rtsa.feature.feature_type IS 'feature类型';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature.name IS 'feature名称';
|
COMMENT ON COLUMN rtsa.feature.name IS 'feature名称';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature.description IS 'feature描述';
|
COMMENT ON COLUMN rtsa.feature.description IS 'feature描述';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature.is_published IS '是否上架';
|
COMMENT ON COLUMN rtsa.feature.config IS 'feature配置';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature.creator_id IS '创建用户id';
|
COMMENT ON COLUMN rtsa.feature.is_published IS '是否上架';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature.created_at IS '创建时间';
|
COMMENT ON COLUMN rtsa.feature.creator_id IS '创建用户id';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature.updated_at IS '更新时间';
|
COMMENT ON COLUMN rtsa.feature.created_at IS '创建时间';
|
||||||
|
|
||||||
-- 创建仿真feature和发布数据关联表
|
COMMENT ON COLUMN rtsa.feature.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 创建组织功能表
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtss.feature_release_data (
|
rtsa.organization_feature (
|
||||||
feature_id INT NOT NULL, -- 仿真feature id
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
release_data_id INT NOT NULL, -- 发布数据id
|
organization_id INT NOT NULL, -- 组织id
|
||||||
PRIMARY KEY (feature_id, release_data_id),
|
feature_id INT NOT NULL, -- feature id
|
||||||
FOREIGN KEY (feature_id) REFERENCES rtss.feature (id) ON DELETE CASCADE,
|
config JSONB NOT NULL, -- 组织功能配置
|
||||||
FOREIGN KEY (release_data_id) REFERENCES rtss.release_data (id) ON DELETE CASCADE
|
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 (feature_id) REFERENCES rtsa.feature (id) ON DELETE CASCADE, -- feature外键
|
||||||
|
FOREIGN KEY (creator_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 创建用户外键
|
||||||
|
FOREIGN KEY (updater_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 更新用户外键
|
||||||
|
UNIQUE(organization_id, feature_id) -- 组织id和feature id唯一
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 注释仿真feature和发布数据关联表
|
-- 创建组织功能组织索引
|
||||||
COMMENT ON TABLE rtss.feature_release_data IS '仿真feature和发布数据关联表';
|
CREATE INDEX ON rtsa.organization_feature (organization_id);
|
||||||
|
|
||||||
-- 注释仿真feature和发布数据关联表字段
|
-- 创建组织功能feature索引
|
||||||
COMMENT ON COLUMN rtss.feature_release_data.feature_id IS '仿真feature id';
|
CREATE INDEX ON rtsa.organization_feature (feature_id);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_release_data.release_data_id IS '发布数据id';
|
-- 创建组织功能配置项索引
|
||||||
|
CREATE INDEX ON rtsa.organization_feature USING GIN (config);
|
||||||
|
|
||||||
-- 创建feature group表
|
-- 创建组织功能创建用户索引
|
||||||
|
CREATE INDEX ON rtsa.organization_feature (creator_id);
|
||||||
|
|
||||||
|
-- 创建组织功能更新用户索引
|
||||||
|
CREATE INDEX ON rtsa.organization_feature (updater_id);
|
||||||
|
|
||||||
|
-- 注释组织功能表
|
||||||
|
COMMENT ON TABLE rtsa.organization_feature IS '组织功能表';
|
||||||
|
|
||||||
|
-- 注释组织功能表字段
|
||||||
|
COMMENT ON COLUMN rtsa.organization_feature.id IS 'id 自增主键';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_feature.organization_id IS '组织id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_feature.feature_id IS 'feature id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_feature.config IS '组织功能配置';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_feature.creator_id IS '创建用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_feature.created_at IS '创建时间';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_feature.updater_id IS '更新用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.organization_feature.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 创建草稿数据表
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtss.feature_group (
|
rtsa.draft_data (
|
||||||
id SERIAL PRIMARY KEY, -- id 自增主键
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
name VARCHAR(128) NOT NULL UNIQUE, -- feature group名称
|
name VARCHAR(128) NOT NULL, -- 草稿名称
|
||||||
description TEXT NOT NULL, -- feature group描述
|
data_type INT NOT NULL, -- 数据类型
|
||||||
|
options JSONB NULL, -- 数据相关的参数项或配置项
|
||||||
|
data BYTEA, -- 草稿数据
|
||||||
|
default_release_data_id INT NULL, -- 默认发布数据id
|
||||||
|
user_id INT NOT NULL, -- 创建用户id
|
||||||
|
is_shared BOOLEAN NOT NULL DEFAULT FALSE, -- 是否共享
|
||||||
|
created_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 rtsa.user (id) ON DELETE CASCADE, -- 用户外键
|
||||||
|
UNIQUE(name, data_type, user_id) -- 一个用户的某个类型的草稿名称唯一
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建草稿数据用户索引
|
||||||
|
CREATE INDEX ON rtsa.draft_data (user_id);
|
||||||
|
|
||||||
|
-- 创建草稿数据类型索引
|
||||||
|
CREATE INDEX ON rtsa.draft_data (data_type);
|
||||||
|
|
||||||
|
-- 创建草稿数据配置项索引
|
||||||
|
CREATE INDEX ON rtsa.draft_data USING GIN (options);
|
||||||
|
|
||||||
|
-- 注释草稿数据表
|
||||||
|
COMMENT ON TABLE rtsa.draft_data IS '草稿数据表';
|
||||||
|
|
||||||
|
-- 注释草稿数据表字段
|
||||||
|
COMMENT ON COLUMN rtsa.draft_data.id IS 'id 自增主键';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.draft_data.name IS '草稿名称';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.draft_data.data_type IS '数据类型';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.draft_data.options IS '数据相关的参数项或配置项';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.draft_data.data IS '草稿数据';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.draft_data.user_id IS '创建用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.draft_data.is_shared IS '是否共享';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.draft_data.created_at IS '创建时间';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.draft_data.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 创建发布数据表
|
||||||
|
CREATE TABLE
|
||||||
|
rtsa.release_data (
|
||||||
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
|
name VARCHAR(128) NOT NULL, -- 发布数据名称(数据唯一标识)
|
||||||
|
data_type INT NOT NULL, -- 数据类型
|
||||||
|
options JSONB NULL, -- 数据相关的参数项或配置项
|
||||||
|
used_version_id INT NULL, -- 使用的版本数据id
|
||||||
|
user_id INT NOT NULL, -- 发布/更新用户id
|
||||||
|
is_published BOOLEAN NOT NULL DEFAULT TRUE, -- 是否上架
|
||||||
|
created_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 rtsa.user (id) ON DELETE CASCADE, -- 用户外键
|
||||||
|
UNIQUE(data_type, name) -- 数据类型和名称唯一
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建发布数据名称索引
|
||||||
|
CREATE INDEX ON rtsa.release_data (name);
|
||||||
|
|
||||||
|
-- 创建发布数据用户索引
|
||||||
|
CREATE INDEX ON rtsa.release_data (user_id);
|
||||||
|
|
||||||
|
-- 创建发布数据类型索引
|
||||||
|
CREATE INDEX ON rtsa.release_data (data_type);
|
||||||
|
|
||||||
|
-- 创建发布数据配置项索引
|
||||||
|
CREATE INDEX ON rtsa.release_data USING GIN (options);
|
||||||
|
|
||||||
|
-- 注释发布数据表
|
||||||
|
COMMENT ON TABLE rtsa.release_data IS '发布数据表';
|
||||||
|
|
||||||
|
-- 注释发布数据表字段
|
||||||
|
COMMENT ON COLUMN rtsa.release_data.id IS 'id 自增主键';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data.name IS '发布数据名称(数据唯一标识)';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data.data_type IS '数据类型';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data.options IS '数据相关的参数项或配置项';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data.used_version_id IS '使用的版本数据id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data.user_id IS '发布/更新用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data.is_published IS '是否上架';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data.created_at IS '创建时间';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 创建发布数据版本表
|
||||||
|
CREATE TABLE
|
||||||
|
rtsa.release_data_version (
|
||||||
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
|
release_data_id INT NOT NULL, -- 发布数据id
|
||||||
|
options JSONB NULL, -- 数据相关的参数项或配置项
|
||||||
|
data BYTEA NOT NULL, -- 数据
|
||||||
|
description TEXT NOT NULL, -- 版本描述
|
||||||
|
user_id INT NOT NULL, -- 发布用户id
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
|
FOREIGN KEY (user_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 用户外键
|
||||||
|
FOREIGN KEY (release_data_id) REFERENCES rtsa.release_data (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建发布数据版本发布数据索引
|
||||||
|
CREATE INDEX ON rtsa.release_data_version (release_data_id);
|
||||||
|
|
||||||
|
-- 创建发布数据版本用户索引
|
||||||
|
CREATE INDEX ON rtsa.release_data_version (user_id);
|
||||||
|
|
||||||
|
-- 创建发布数据版本配置项索引
|
||||||
|
CREATE INDEX ON rtsa.release_data_version USING GIN (options);
|
||||||
|
|
||||||
|
-- 创建发布数据当前版本外键
|
||||||
|
ALTER TABLE rtsa.release_data ADD FOREIGN KEY (used_version_id) REFERENCES rtsa.release_data_version (id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- 创建草稿数据默认发布数据外键
|
||||||
|
ALTER TABLE rtsa.draft_data ADD FOREIGN KEY (default_release_data_id) REFERENCES rtsa.release_data (id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- 注释发布数据版本表
|
||||||
|
COMMENT ON TABLE rtsa.release_data_version IS '发布数据版本表';
|
||||||
|
|
||||||
|
-- 注释发布数据版本表字段
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_version.id IS 'id 自增主键';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_version.release_data_id IS '发布数据id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_version.options IS '数据相关的参数项或配置项';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_version.data IS '数据';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_version.description IS '版本描述';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_version.user_id IS '发布用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_version.created_at IS '创建时间';
|
||||||
|
|
||||||
|
-- 创建发布数据集表
|
||||||
|
CREATE TABLE
|
||||||
|
rtsa.release_data_set (
|
||||||
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
|
data_set_type INT NOT NULL, -- 数据集类型
|
||||||
|
name VARCHAR(128) NOT NULL, -- 发布数据集名称
|
||||||
|
description TEXT NOT NULL, -- 发布数据集描述
|
||||||
|
config JSONB NULL, -- 数据集相关配置项
|
||||||
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
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
updater_id INT NOT NULL, -- 更新用户id
|
updater_id INT NOT NULL, -- 更新用户id
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()', -- 创建时间
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()' -- 更新时间
|
FOREIGN KEY (creator_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 创建用户外键
|
||||||
|
FOREIGN KEY (updater_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 更新用户外键
|
||||||
|
UNIQUE(data_set_type, name) -- 数据集类型+name唯一
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 注释仿真feature group表
|
-- 创建发布数据集类型索引
|
||||||
COMMENT ON TABLE rtss.feature_group IS 'feature group表';
|
CREATE INDEX ON rtsa.release_data_set (data_set_type);
|
||||||
|
|
||||||
-- 注释仿真feature group表字段
|
-- 创建发布数据集名称索引
|
||||||
COMMENT ON COLUMN rtss.feature_group.id IS 'id 自增主键';
|
CREATE INDEX ON rtsa.release_data_set (name);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_group.name IS 'feature group名称';
|
-- 创建发布数据集配置项索引
|
||||||
|
CREATE INDEX ON rtsa.release_data_set USING GIN (config);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_group.description IS 'feature group描述';
|
-- 创建发布数据集创建用户索引
|
||||||
|
CREATE INDEX ON rtsa.release_data_set (creator_id);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_group.is_published IS '是否上架';
|
-- 创建发布数据集更新用户索引
|
||||||
|
CREATE INDEX ON rtsa.release_data_set (updater_id);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_group.creator_id IS '创建用户id';
|
-- 创建发布数据集索引
|
||||||
|
CREATE INDEX ON rtsa.release_data_set (data_set_type, name);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_group.created_at IS '创建时间';
|
-- 注释发布数据集表
|
||||||
|
COMMENT ON TABLE rtsa.release_data_set IS '发布数据集表';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_group.updated_at IS '更新时间';
|
-- 注释发布数据集表字段
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set.id IS 'id 自增主键';
|
||||||
|
|
||||||
-- 创建feature group和feature关联表
|
COMMENT ON COLUMN rtsa.release_data_set.data_set_type IS '数据集类型';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set.name IS '发布数据集名称';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set.description IS '发布数据集描述';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set.config IS '数据集相关配置项';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set.is_published IS '是否上架';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set.creator_id IS '创建用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set.created_at IS '创建时间';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set.updater_id IS '更新用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 创建发布数据集数据表
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtss.feature_group_feature (
|
rtsa.release_data_set_data (
|
||||||
feature_group_id INT NOT NULL, -- feature group id
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
feature_id INT NOT NULL, -- feature id
|
release_data_set_id INT NOT NULL, -- 发布数据集id
|
||||||
PRIMARY KEY (feature_id, feature_group_id),
|
release_data_id INT NOT NULL, -- 发布数据id
|
||||||
FOREIGN KEY (feature_id) REFERENCES rtss.feature (id) ON DELETE CASCADE,
|
order_index INT NOT NULL, -- 排序索引
|
||||||
FOREIGN KEY (feature_group_id) REFERENCES rtss.feature_group (id) ON DELETE CASCADE
|
FOREIGN KEY (release_data_set_id) REFERENCES rtsa.release_data_set (id) ON DELETE CASCADE, -- 发布数据集外键
|
||||||
|
FOREIGN KEY (release_data_id) REFERENCES rtsa.release_data (id) ON DELETE CASCADE, -- 发布数据外键
|
||||||
|
UNIQUE(release_data_set_id, release_data_id) -- 发布数据集id和发布数据id唯一
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 注释仿真feature group和feature关联表
|
-- 创建发布数据集数据发布数据集索引
|
||||||
COMMENT ON TABLE rtss.feature_group_feature IS '仿真feature group和feature关联表';
|
CREATE INDEX ON rtsa.release_data_set_data (release_data_set_id);
|
||||||
|
|
||||||
-- 创建用户feature配置表
|
-- 创建发布数据集数据发布数据索引
|
||||||
|
CREATE INDEX ON rtsa.release_data_set_data (release_data_id);
|
||||||
|
|
||||||
|
-- 注释发布数据集数据表
|
||||||
|
COMMENT ON TABLE rtsa.release_data_set_data IS '发布数据集数据表';
|
||||||
|
|
||||||
|
-- 注释发布数据集数据表字段
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set_data.id IS 'id 自增主键';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set_data.release_data_set_id IS '发布数据集id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set_data.release_data_id IS '发布数据id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.release_data_set_data.order_index IS '排序索引';
|
||||||
|
|
||||||
|
-- 创建用户数据表
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtss.feature_config (
|
rtsa.user_data (
|
||||||
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
|
data_type INT NOT NULL, -- 数据类型
|
||||||
config BYTEA NOT NULL, -- 配置
|
data JSONB NOT NULL, -- 数据
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()', -- 创建时间
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now()', -- 更新时间
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
|
||||||
FOREIGN KEY (feature_id) REFERENCES rtss.feature (id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES rtsa.user (id) ON DELETE CASCADE -- 用户外键
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 注释用户feature配置表
|
-- 创建用户数据用户索引
|
||||||
COMMENT ON TABLE rtss.feature_config IS '用户feature配置表';
|
CREATE INDEX ON rtsa.user_data (user_id);
|
||||||
|
|
||||||
-- 注释用户feature配置表字段
|
-- 创建用户数据类型索引
|
||||||
COMMENT ON COLUMN rtss.feature_config.id IS 'id 自增主键';
|
CREATE INDEX ON rtsa.user_data (data_type);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_config.user_id IS '用户id';
|
-- 创建用户数据配置项索引
|
||||||
|
CREATE INDEX ON rtsa.user_data USING GIN (data);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_config.feature_id IS '仿真feature id';
|
-- 创建用户数据索引
|
||||||
|
CREATE INDEX ON rtsa.user_data (user_id, data_type);
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_config.config IS '配置';
|
-- 注释用户数据表
|
||||||
|
COMMENT ON TABLE rtsa.user_data IS '用户数据表';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_config.created_at IS '创建时间';
|
-- 注释用户数据表字段
|
||||||
|
COMMENT ON COLUMN rtsa.user_data.id IS 'id 自增主键';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtss.feature_config.updated_at IS '更新时间';
|
COMMENT ON COLUMN rtsa.user_data.user_id IS '用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user_data.data_type IS '数据类型';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user_data.data IS '数据';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user_data.created_at IS '创建时间';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user_data.updated_at IS '更新时间';
|
||||||
|
|
||||||
|
-- 创建用户操作日志表
|
||||||
|
CREATE TABLE
|
||||||
|
rtsa.user_operation_log (
|
||||||
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
|
organization_id INT NOT NULL, -- 组织id
|
||||||
|
user_id INT NOT NULL, -- 用户id
|
||||||
|
log_type INT NOT NULL, -- 日志类型
|
||||||
|
log_data JSONB NOT NULL, -- 日志数据
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES rtsa.organization (id) ON DELETE CASCADE, -- 组织外键
|
||||||
|
FOREIGN KEY (user_id) REFERENCES rtsa.user (id) ON DELETE CASCADE -- 用户外键
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建用户操作日志组织索引
|
||||||
|
CREATE INDEX ON rtsa.user_operation_log (organization_id);
|
||||||
|
|
||||||
|
-- 创建用户操作日志用户索引
|
||||||
|
CREATE INDEX ON rtsa.user_operation_log (user_id);
|
||||||
|
|
||||||
|
-- 创建用户操作日志类型索引
|
||||||
|
CREATE INDEX ON rtsa.user_operation_log (log_type);
|
||||||
|
|
||||||
|
-- 创建用户操作日志数据索引
|
||||||
|
CREATE INDEX ON rtsa.user_operation_log USING GIN (log_data);
|
||||||
|
|
||||||
|
-- 创建用户操作日志索引
|
||||||
|
CREATE INDEX ON rtsa.user_operation_log (organization_id, user_id, log_type, created_at);
|
||||||
|
|
||||||
|
-- 注释用户操作日志表
|
||||||
|
COMMENT ON TABLE rtsa.user_operation_log IS '用户操作日志表';
|
||||||
|
|
||||||
|
-- 注释用户操作日志表字段
|
||||||
|
COMMENT ON COLUMN rtsa.user_operation_log.id IS 'id 自增主键';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user_operation_log.organization_id IS '组织id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user_operation_log.user_id IS '用户id';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user_operation_log.log_type IS '日志类型';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user_operation_log.log_data IS '日志记录数据';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user_operation_log.created_at IS '创建时间';
|
||||||
|
1
rtsa-proto-msg
Submodule
1
rtsa-proto-msg
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit a30bf52339620075e8d666be363eb3d93d532656
|
@ -1 +0,0 @@
|
|||||||
Subproject commit 4e447beffa31c94b30bcee57a7cf229dcf01351f
|
|
24
simulation/Cargo.toml
Normal file
24
simulation/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "simulation"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy_core = { workspace = true }
|
||||||
|
bevy_ecs = { workspace = true }
|
||||||
|
bevy_app = { workspace = true }
|
||||||
|
bevy_time = { workspace = true }
|
||||||
|
rayon = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
lazy_static = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
config = { workspace = true }
|
||||||
|
clap = { workspace = true, features = ["derive"] }
|
||||||
|
enum_dispatch = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
|
||||||
|
rtsa_log = { path = "../crates/rtsa_log" }
|
||||||
|
rtsa_dto = { path = "../crates/rtsa_dto" }
|
||||||
|
rtsa_db = { path = "../crates/rtsa_db" }
|
||||||
|
rtsa_mqtt = { path = "../crates/rtsa_mqtt" }
|
9
simulation/conf/default.toml
Normal file
9
simulation/conf/default.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[database]
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "debug"
|
||||||
|
|
||||||
|
[mqtt]
|
||||||
|
url = "http://localhost:8080"
|
||||||
|
username = "admin"
|
||||||
|
password = "admin"
|
7
simulation/conf/dev.toml
Normal file
7
simulation/conf/dev.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[database]
|
||||||
|
url = "postgresql://joylink:Joylink@0503@localhost:5432/joylink"
|
||||||
|
|
||||||
|
[mqtt]
|
||||||
|
url = "tcp://localhost:1883"
|
||||||
|
username = "rtsa"
|
||||||
|
password = "Joylink@0503"
|
10
simulation/conf/local_test.toml
Normal file
10
simulation/conf/local_test.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[database]
|
||||||
|
url = "postgresql://joylink:Joylink@0503@10.11.11.2:5432/joylink"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "debug"
|
||||||
|
|
||||||
|
[mqtt]
|
||||||
|
url = "tcp://192.168.3.233:1883"
|
||||||
|
username = "rtsa"
|
||||||
|
password = "Joylink@0503"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user