mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat: 看板SQL与布局支持数据库持久化
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use parking_lot::RwLock;
|
||||
use serde::Deserialize;
|
||||
use sea_orm::{ConnectionTrait, DatabaseConnection, Statement};
|
||||
use serde_json::{Map, Value as JsonValue};
|
||||
use sqlx::{Column, Row, SqlitePool};
|
||||
use std::collections::HashSet;
|
||||
@@ -24,11 +25,23 @@ enum QueryBackend {
|
||||
Postgres,
|
||||
}
|
||||
|
||||
fn check_view_permission(state: &Arc<RwLock<AppState>>, sender_id: Option<u32>) -> bool {
|
||||
fn check_permission(
|
||||
state: &Arc<RwLock<AppState>>,
|
||||
sender_id: Option<u32>,
|
||||
level: PermissionLevel,
|
||||
) -> bool {
|
||||
let state_guard = state.read();
|
||||
let mut permissions = state_guard.permissions.write();
|
||||
let id = sender_id.unwrap_or(0);
|
||||
permissions.require_permission(id, PermissionLevel::View)
|
||||
permissions.require_permission(id, level)
|
||||
}
|
||||
|
||||
fn check_view_permission(state: &Arc<RwLock<AppState>>, sender_id: Option<u32>) -> bool {
|
||||
check_permission(state, sender_id, PermissionLevel::View)
|
||||
}
|
||||
|
||||
fn check_admin_permission(state: &Arc<RwLock<AppState>>, sender_id: Option<u32>) -> bool {
|
||||
check_permission(state, sender_id, PermissionLevel::Admin)
|
||||
}
|
||||
|
||||
fn contains_forbidden_keyword(sql: &str) -> bool {
|
||||
@@ -92,6 +105,87 @@ fn parse_limit(limit: Option<u64>) -> u64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_sql(value: &str) -> String {
|
||||
value.replace('\'', "''")
|
||||
}
|
||||
|
||||
fn now_iso() -> String {
|
||||
chrono::Utc::now()
|
||||
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn normalize_board_configs(value: JsonValue) -> JsonValue {
|
||||
if value.is_array() {
|
||||
value
|
||||
} else {
|
||||
JsonValue::Array(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_board_configs_table(conn: &DatabaseConnection) -> Result<(), String> {
|
||||
let sql = r#"
|
||||
CREATE TABLE IF NOT EXISTS board_configs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
config_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"#
|
||||
.to_string();
|
||||
|
||||
conn.execute(Statement::from_string(conn.get_database_backend(), sql))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_board_configs_raw(conn: &DatabaseConnection) -> Result<Option<String>, String> {
|
||||
let sql = "SELECT config_json FROM board_configs WHERE id = 1".to_string();
|
||||
let row = conn
|
||||
.query_one(Statement::from_string(conn.get_database_backend(), sql))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match row {
|
||||
Some(r) => r
|
||||
.try_get("", "config_json")
|
||||
.map(Some)
|
||||
.map_err(|e| e.to_string()),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn upsert_board_configs_raw(conn: &DatabaseConnection, config_json: &str) -> Result<(), String> {
|
||||
let config_escaped = escape_sql(config_json);
|
||||
let now_escaped = escape_sql(&now_iso());
|
||||
let sql = format!(
|
||||
"INSERT INTO board_configs (id, config_json, updated_at) VALUES (1, '{}', '{}') ON CONFLICT(id) DO UPDATE SET config_json = EXCLUDED.config_json, updated_at = EXCLUDED.updated_at",
|
||||
config_escaped, now_escaped
|
||||
);
|
||||
|
||||
conn.execute(Statement::from_string(conn.get_database_backend(), sql))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_legacy_board_configs_from_settings(
|
||||
state: &Arc<RwLock<AppState>>,
|
||||
conn: Option<DatabaseConnection>,
|
||||
) -> Result<JsonValue, String> {
|
||||
let state_guard = state.read();
|
||||
let mut settings = state_guard.settings.write();
|
||||
settings.attach_db(conn);
|
||||
settings.initialize().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let legacy = match settings.get_value(SettingsKey::DashboardsConfig) {
|
||||
SettingsValue::Json(v) => v,
|
||||
_ => JsonValue::Array(vec![]),
|
||||
};
|
||||
|
||||
Ok(normalize_board_configs(legacy))
|
||||
}
|
||||
|
||||
fn sqlite_db_path(app_handle: &AppHandle) -> Result<String, String> {
|
||||
if cfg!(all(debug_assertions, desktop)) {
|
||||
return Ok("data.sql".to_string());
|
||||
@@ -200,6 +294,68 @@ async fn query_postgres(sql: &str, pg_url: &str) -> Result<Vec<JsonValue>, Strin
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn board_get_configs(
|
||||
sender_id: Option<u32>,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<JsonValue>, String> {
|
||||
if !check_view_permission(&state, sender_id) {
|
||||
return Ok(IpcResponse::error("Permission denied: view required"));
|
||||
}
|
||||
|
||||
let db_conn = {
|
||||
let state_guard = state.read();
|
||||
let conn = state_guard.db.read().clone();
|
||||
conn
|
||||
};
|
||||
let Some(conn) = db_conn else {
|
||||
return Ok(IpcResponse::error("Database not connected"));
|
||||
};
|
||||
|
||||
ensure_board_configs_table(&conn).await?;
|
||||
|
||||
if let Some(raw) = get_board_configs_raw(&conn).await? {
|
||||
let parsed: JsonValue = serde_json::from_str(&raw).unwrap_or(JsonValue::Array(vec![]));
|
||||
return Ok(IpcResponse::success(normalize_board_configs(parsed)));
|
||||
}
|
||||
|
||||
let legacy = get_legacy_board_configs_from_settings(&state, Some(conn.clone())).await?;
|
||||
if legacy.as_array().map(|v| !v.is_empty()).unwrap_or(false) {
|
||||
let raw = serde_json::to_string(&legacy).map_err(|e| e.to_string())?;
|
||||
upsert_board_configs_raw(&conn, &raw).await?;
|
||||
}
|
||||
|
||||
Ok(IpcResponse::success(legacy))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn board_save_configs(
|
||||
configs: JsonValue,
|
||||
sender_id: Option<u32>,
|
||||
state: State<'_, Arc<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<()>, String> {
|
||||
if !check_admin_permission(&state, sender_id) {
|
||||
return Ok(IpcResponse::error("Permission denied: admin required"));
|
||||
}
|
||||
|
||||
let normalized = normalize_board_configs(configs);
|
||||
let raw = serde_json::to_string(&normalized).map_err(|e| e.to_string())?;
|
||||
|
||||
let db_conn = {
|
||||
let state_guard = state.read();
|
||||
let conn = state_guard.db.read().clone();
|
||||
conn
|
||||
};
|
||||
let Some(conn) = db_conn else {
|
||||
return Ok(IpcResponse::error("Database not connected"));
|
||||
};
|
||||
|
||||
ensure_board_configs_table(&conn).await?;
|
||||
upsert_board_configs_raw(&conn, &raw).await?;
|
||||
|
||||
Ok(IpcResponse::success_empty())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn board_query_sql(
|
||||
params: BoardSqlQueryParams,
|
||||
|
||||
@@ -17,6 +17,7 @@ impl Migration {
|
||||
Self::create_score_events_table(conn, is_sqlite).await?;
|
||||
Self::create_settlements_table(conn, is_sqlite).await?;
|
||||
Self::create_settings_table(conn, is_sqlite).await?;
|
||||
Self::create_board_configs_table(conn, is_sqlite).await?;
|
||||
Self::create_tags_table(conn, is_sqlite).await?;
|
||||
Self::create_student_tags_table(conn, is_sqlite).await?;
|
||||
Self::create_reward_settings_table(conn, is_sqlite).await?;
|
||||
@@ -77,6 +78,17 @@ impl Migration {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_board_configs_table(
|
||||
conn: &impl ConnectionTrait,
|
||||
sqlite: bool,
|
||||
) -> Result<(), DbErr> {
|
||||
let sql = get_create_board_configs_table_sql(sqlite);
|
||||
conn.execute(Statement::from_string(Self::get_db_backend(sqlite), sql))
|
||||
.await?;
|
||||
info!("Created board_configs table");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_tags_table(conn: &impl ConnectionTrait, sqlite: bool) -> Result<(), DbErr> {
|
||||
let sql = get_create_tags_table_sql(sqlite);
|
||||
conn.execute(Statement::from_string(Self::get_db_backend(sqlite), sql))
|
||||
@@ -315,6 +327,7 @@ impl Migration {
|
||||
TABLE_REWARD_REDEMPTIONS,
|
||||
TABLE_REWARD_SETTINGS,
|
||||
TABLE_SETTINGS,
|
||||
TABLE_BOARD_CONFIGS,
|
||||
];
|
||||
|
||||
let db_backend = Self::get_db_backend(sqlite);
|
||||
|
||||
@@ -3,6 +3,7 @@ pub const TABLE_REASONS: &str = "reasons";
|
||||
pub const TABLE_SCORE_EVENTS: &str = "score_events";
|
||||
pub const TABLE_SETTLEMENTS: &str = "settlements";
|
||||
pub const TABLE_SETTINGS: &str = "settings";
|
||||
pub const TABLE_BOARD_CONFIGS: &str = "board_configs";
|
||||
pub const TABLE_TAGS: &str = "tags";
|
||||
pub const TABLE_STUDENT_TAGS: &str = "student_tags";
|
||||
pub const TABLE_REWARD_SETTINGS: &str = "reward_settings";
|
||||
@@ -57,6 +58,13 @@ pub mod settings {
|
||||
pub const VALUE: &str = "value";
|
||||
}
|
||||
|
||||
pub mod board_configs {
|
||||
pub const TABLE: &str = "board_configs";
|
||||
pub const ID: &str = "id";
|
||||
pub const CONFIG_JSON: &str = "config_json";
|
||||
pub const UPDATED_AT: &str = "updated_at";
|
||||
}
|
||||
|
||||
pub mod tags {
|
||||
pub const TABLE: &str = "tags";
|
||||
pub const ID: &str = "id";
|
||||
@@ -287,6 +295,28 @@ pub fn get_create_settings_table_sql(sqlite: bool) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_create_board_configs_table_sql(sqlite: bool) -> String {
|
||||
if sqlite {
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS board_configs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
config_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"#
|
||||
.to_string()
|
||||
} else {
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS board_configs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
config_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"#
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_create_tags_table_sql(sqlite: bool) -> String {
|
||||
if sqlite {
|
||||
r#"
|
||||
|
||||
@@ -83,6 +83,8 @@ pub fn run() {
|
||||
auto_score_toggle_rule,
|
||||
auto_score_get_status,
|
||||
auto_score_sort_rules,
|
||||
board_get_configs,
|
||||
board_save_configs,
|
||||
board_query_sql,
|
||||
log_query,
|
||||
log_clear,
|
||||
|
||||
Reference in New Issue
Block a user