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`)? 用户需求: { {在这里粘贴用户需求} } 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 4a23fc0..4cc014f 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..ed8ced0 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) @@ -835,7 +835,14 @@ ORDER BY reward_points DESC, score DESC`, > {rankBadge && (
{rankBadge}
@@ -1009,9 +1016,15 @@ ORDER BY reward_points DESC, score DESC`, title={{list.name}} extra={ - + + aria-label={t("board.removeList")} + title={t("board.removeList")} + /> } @@ -1292,6 +1307,16 @@ ORDER BY reward_points DESC, score DESC`, fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", }} /> + + 豆包智能体一句话生成查询 + + + 扣子智能体一句话生成查询 + )} 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"),