From 57dfa389648c25a2a28b8724c10c5ce2531791ec Mon Sep 17 00:00:00 2001 From: JSR Date: Tue, 24 Mar 2026 19:34:30 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20=E5=BC=BA=E5=8C=96=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?SQL=E6=8F=90=E7=A4=BA=E8=AF=8D=E9=81=BF=E5=85=8D=E5=AD=A6?= =?UTF-8?q?=E7=94=9F=E5=90=8D=E5=AD=97=E6=AE=B5=E8=AF=AF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/看板SQL生成提示词.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/resources/看板SQL生成提示词.md b/resources/看板SQL生成提示词.md index 993abbc..a38ab20 100644 --- a/resources/看板SQL生成提示词.md +++ b/resources/看板SQL生成提示词.md @@ -65,17 +65,27 @@ 9. `reward_redemptions` - `id`, `uuid`, `student_name`, `reward_id`, `reward_name`, `cost_points`, `redeemed_at` -## 5) 看板展示字段命名约定(尽量遵守) +## 5) 字段名使用铁律(必须遵守,防止列不存在) +1. `SELECT`、`WHERE`、`GROUP BY`、`ORDER BY`、`JOIN ON` 中引用的字段名,必须来自上面的“可用业务表与字段”原样字段,严禁臆造字段。 +2. “展示名/别名”不等于真实字段名。若需展示为 `student_name`,必须使用别名,不可把别名当字段直接查询。 +3. 关键映射(高频易错): + - `students` 表没有 `student_name`,只有 `name`。正确写法:`students.name AS student_name`(或 `s.name AS student_name`) + - `score_events` 表有 `student_name` + - `reward_redemptions` 表有 `student_name` +4. 错误示例(禁止生成):`SELECT student_name, score FROM students` +5. 正确示例(优先生成):`SELECT name AS student_name, score FROM students` + +## 6) 看板展示字段命名约定(尽量遵守) - 学生名统一命名为:`student_name` - 常见指标命名建议:`score`, `reward_points`, `week_change`, `week_deducted`, `answered_count` -## 6) 生成策略 +## 7) 生成策略 1. 排行类需求必须有 `ORDER BY`。 2. 聚合类需求必须有清晰别名。 3. 对可能为空的数据优先使用 `COALESCE`。 4. 默认不写 `LIMIT`(系统外层会限制);除非用户明确要求更小结果集。 -## 7) 自然周规则(允许生成上个自然周 SQL) +## 8) 自然周规则(允许生成上个自然周 SQL) 当用户要求“上个自然周/本自然周”时,必须使用模板变量区间表达,不要自行计算数据库日期函数: - 上个自然周:`event_time >= '{ {last_week_start} }' AND event_time < '{ {this_week_start} }'` - 本自然周:`event_time >= '{ {this_week_start} }'` @@ -85,7 +95,7 @@ 2. 不要发明其它周边界变量。 3. 避免使用 SQLite/PostgreSQL 方言日期函数(如 `strftime`、`date_trunc`)以保证跨库兼容。 -## 8) 输出前自检清单(必须全部为“是”) +## 9) 输出前自检清单(必须全部为“是”) - 是否只用了 `SELECT/WITH`? - 是否没有 `;` 和注释? - 是否没有禁用关键词? @@ -93,6 +103,7 @@ - 模板变量是否都加了单引号? - 是否没有系统表(如 `sqlite_master`)? - 是否只用了给定业务表? +- 是否所有字段都能在对应表字段清单中找到(尤其检查 `students.name` vs `students.student_name`)? 用户需求: { {在这里粘贴用户需求} } From c525b3bd31691f5f34a4cc954a20d1d5582cd825 Mon Sep 17 00:00:00 2001 From: JSR Date: Tue, 24 Mar 2026 19:41:35 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=E7=9C=8B=E6=9D=BFSQL=E4=B8=8E?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E6=94=AF=E6=8C=81=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/board.rs | 160 +++++++++++++++++++++++++++++++- src-tauri/src/db/migration.rs | 13 +++ src-tauri/src/db/schema.rs | 30 ++++++ src-tauri/src/lib.rs | 2 + src/components/BoardManager.tsx | 4 +- src/preload/types.ts | 4 + 6 files changed, 209 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands/board.rs b/src-tauri/src/commands/board.rs index 3646836..6459962 100644 --- a/src-tauri/src/commands/board.rs +++ b/src-tauri/src/commands/board.rs @@ -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>, sender_id: Option) -> bool { +fn check_permission( + state: &Arc>, + sender_id: Option, + 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>, sender_id: Option) -> bool { + check_permission(state, sender_id, PermissionLevel::View) +} + +fn check_admin_permission(state: &Arc>, sender_id: Option) -> 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 { } } +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, 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>, + conn: Option, +) -> Result { + 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 { 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, Strin Ok(data) } +#[tauri::command] +pub async fn board_get_configs( + sender_id: Option, + state: State<'_, Arc>>, +) -> Result, 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, + state: State<'_, Arc>>, +) -> Result, 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, diff --git a/src-tauri/src/db/migration.rs b/src-tauri/src/db/migration.rs index 4380077..157e1bb 100644 --- a/src-tauri/src/db/migration.rs +++ b/src-tauri/src/db/migration.rs @@ -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); diff --git a/src-tauri/src/db/schema.rs b/src-tauri/src/db/schema.rs index 28441b4..9368c0b 100644 --- a/src-tauri/src/db/schema.rs +++ b/src-tauri/src/db/schema.rs @@ -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#" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3603139..192302e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 60c0720..8e63926 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -444,7 +444,7 @@ ORDER BY reward_points DESC, score DESC`, setSaving(true) try { - const res = await (window as any).api.setSetting("dashboards_config", nextBoards) + const res = await (window as any).api.boardSaveConfigs(nextBoards) if (!res?.success) { messageApi.error(res?.message || t("board.saveFailed")) } @@ -475,7 +475,7 @@ ORDER BY reward_points DESC, score DESC`, setLoading(true) try { - const res = await (window as any).api.getSetting("dashboards_config") + const res = await (window as any).api.boardGetConfigs() if (res?.success) { const normalized = normalizeBoards(res?.data) setBoards(normalized) diff --git a/src/preload/types.ts b/src/preload/types.ts index 0e8dde9..fa07e73 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -155,6 +155,10 @@ const api = { limit?: number }): Promise<{ success: boolean; data: any[]; message?: string }> => invoke("board_query_sql", { params }), + boardGetConfigs: (): Promise<{ success: boolean; data: any[]; message?: string }> => + invoke("board_get_configs"), + boardSaveConfigs: (configs: any[]): Promise<{ success: boolean; message?: string }> => + invoke("board_save_configs", { configs }), // Settlement querySettlements: (): Promise<{ success: boolean; data: any[] }> => invoke("db_settlement_query"), From 4dc5a9f58cc24428d759eaf2f75b0ff3d67b2e9e Mon Sep 17 00:00:00 2001 From: JSR Date: Tue, 24 Mar 2026 19:45:27 +0800 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=E5=9C=A8=E7=9C=8B=E6=9D=BFSQL?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A1=86=E4=B8=8B=E6=96=B0=E5=A2=9E=E8=B1=86?= =?UTF-8?q?=E5=8C=85=E6=99=BA=E8=83=BD=E4=BD=93=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 8e63926..63c1037 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -1292,6 +1292,9 @@ ORDER BY reward_points DESC, score DESC`, fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", }} /> + + 豆包智能体一句话生成查询 + )} From d56f5e24867bb1ef17a36f93a27a1fa6b4a554da Mon Sep 17 00:00:00 2001 From: JSR Date: Tue, 24 Mar 2026 19:51:07 +0800 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=E5=9C=A8=E7=9C=8B=E6=9D=BFSQL?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E4=B8=AD=E6=96=B0=E5=A2=9E=E6=89=A3=E5=AD=90?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 63c1037..7ef1129 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -1295,6 +1295,13 @@ ORDER BY reward_points DESC, score DESC`, 豆包智能体一句话生成查询 + + 扣子智能体一句话生成查询 + )} From 03feee6e6bc9ce8599143da5a66daec82984be60 Mon Sep 17 00:00:00 2001 From: JSR Date: Tue, 24 Mar 2026 19:54:19 +0800 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=E7=9C=8B=E6=9D=BF=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=BC=96=E8=BE=91=E5=92=8C=E5=88=A0=E9=99=A4=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=9B=BE=E6=A0=87=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 7ef1129..3f3e382 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -1009,9 +1009,15 @@ ORDER BY reward_points DESC, score DESC`, title={{list.name}} extra={ - + + aria-label={t("board.removeList")} + title={t("board.removeList")} + /> } From bb5d5eb2534052ea728e087f994f0b7ea4f3e997 Mon Sep 17 00:00:00 2001 From: JSR Date: Tue, 24 Mar 2026 20:00:20 +0800 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?=E5=AD=A6=E7=94=9F=E5=8D=A1=E7=89=87=E6=8E=92=E5=90=8D=E6=A0=87?= =?UTF-8?q?=E8=AF=86=E5=88=B0=E4=B8=8A=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 3f3e382..d79c1d5 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -835,7 +835,14 @@ ORDER BY reward_points DESC, score DESC`, > {rankBadge && (
{rankBadge}
From 84cdc5f2fd9aa51f608680a5e3e2726665016fc5 Mon Sep 17 00:00:00 2001 From: JSR Date: Tue, 24 Mar 2026 20:02:21 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?=E5=A5=96=E7=89=8C=E4=BD=8D=E7=BD=AE=E4=B8=BA=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E9=A1=B6=E9=83=A8=E5=B7=A6=E4=BE=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index d79c1d5..7e20223 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -836,12 +836,11 @@ ORDER BY reward_points DESC, score DESC`, {rankBadge && (
{rankBadge} From 81fe807020fa20d38ea22f89a7dfbfcd5384efc2 Mon Sep 17 00:00:00 2001 From: JSR Date: Tue, 24 Mar 2026 20:03:21 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?=E5=A5=96=E7=89=8C=E5=88=B0=E5=8D=A1=E7=89=87=E5=8F=B3=E4=BE=A7?= =?UTF-8?q?=E5=B9=B6=E6=94=BE=E5=A4=A7=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BoardManager.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/BoardManager.tsx b/src/components/BoardManager.tsx index 7e20223..ed8ced0 100644 --- a/src/components/BoardManager.tsx +++ b/src/components/BoardManager.tsx @@ -836,11 +836,12 @@ ORDER BY reward_points DESC, score DESC`, {rankBadge && (
{rankBadge}