152 lines
5.7 KiB
Rust
152 lines
5.7 KiB
Rust
//! 会话映射存储(IM chat_id ↔ DevFlow conv_id,SQLite + WAL)
|
|
//!
|
|
//! 建表即带 owner_user_id/platform 列(P0 预留项 ⑤,当前恒默认值,
|
|
//! S2 多人场景免迁表)。
|
|
//!
|
|
//! 表结构:
|
|
//! ```sql
|
|
//! CREATE TABLE IF NOT EXISTS session_map (
|
|
//! platform TEXT NOT NULL, -- feishu/wecom/dingtalk/web
|
|
//! chat_id TEXT NOT NULL, -- IM 侧会话(单聊 user_id / 群聊 chat_id)
|
|
//! conv_id TEXT NOT NULL, -- DevFlow 会话 ID
|
|
//! owner_user_id TEXT NOT NULL DEFAULT '',-- 预留 S2 多人归属(当前恒 '')
|
|
//! channel_id TEXT NOT NULL DEFAULT '',-- 路由到的桌面端 channel(预留项 ⑥)
|
|
//! created_at INTEGER NOT NULL,
|
|
//! updated_at INTEGER NOT NULL,
|
|
//! PRIMARY KEY (platform, chat_id)
|
|
//! );
|
|
//! ```
|
|
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
use rusqlite::Connection;
|
|
|
|
/// 单条会话映射(字段预留 S2 多人/多桌面端,当前核心只读 conv_id)
|
|
#[derive(Debug, Clone)]
|
|
#[allow(dead_code)]
|
|
pub struct SessionMap {
|
|
pub platform: String,
|
|
pub chat_id: String,
|
|
pub conv_id: String,
|
|
pub owner_user_id: String,
|
|
pub channel_id: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SessionStore {
|
|
conn: Arc<std::sync::Mutex<Connection>>,
|
|
}
|
|
|
|
impl SessionStore {
|
|
/// 打开(或创建)库文件并确保表存在。WAL 模式(与桌面端一致)。
|
|
pub fn open(path: &Path) -> anyhow::Result<Self> {
|
|
let conn = Connection::open(path)?;
|
|
conn.pragma_update(None, "journal_mode", "WAL")?;
|
|
conn.execute_batch(
|
|
"CREATE TABLE IF NOT EXISTS session_map (
|
|
platform TEXT NOT NULL,
|
|
chat_id TEXT NOT NULL,
|
|
conv_id TEXT NOT NULL,
|
|
owner_user_id TEXT NOT NULL DEFAULT '',
|
|
channel_id TEXT NOT NULL DEFAULT '',
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
PRIMARY KEY (platform, chat_id)
|
|
);",
|
|
)?;
|
|
Ok(Self { conn: Arc::new(std::sync::Mutex::new(conn)) })
|
|
}
|
|
|
|
/// 内存库(测试/演示)
|
|
#[allow(dead_code)]
|
|
pub fn open_in_memory() -> anyhow::Result<Self> {
|
|
let conn = Connection::open_in_memory()?;
|
|
conn.execute_batch(
|
|
"CREATE TABLE IF NOT EXISTS session_map (
|
|
platform TEXT NOT NULL, chat_id TEXT NOT NULL, conv_id TEXT NOT NULL,
|
|
owner_user_id TEXT NOT NULL DEFAULT '', channel_id TEXT NOT NULL DEFAULT '',
|
|
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
|
|
PRIMARY KEY (platform, chat_id)
|
|
);",
|
|
)?;
|
|
Ok(Self { conn: Arc::new(std::sync::Mutex::new(conn)) })
|
|
}
|
|
|
|
/// 查 IM 会话映射的 conv_id(未映射返回 None)
|
|
pub fn get_conv(&self, platform: &str, chat_id: &str) -> Option<SessionMap> {
|
|
let conn = self.conn.lock().ok()?;
|
|
conn.query_row(
|
|
"SELECT platform, chat_id, conv_id, owner_user_id, channel_id FROM session_map \
|
|
WHERE platform = ?1 AND chat_id = ?2",
|
|
[platform, chat_id],
|
|
|row| {
|
|
Ok(SessionMap {
|
|
platform: row.get(0)?,
|
|
chat_id: row.get(1)?,
|
|
conv_id: row.get(2)?,
|
|
owner_user_id: row.get(3)?,
|
|
channel_id: row.get(4)?,
|
|
})
|
|
},
|
|
)
|
|
.ok()
|
|
}
|
|
|
|
/// 写/更新映射(upsert)
|
|
pub fn set_conv(&self, platform: &str, chat_id: &str, conv_id: &str, channel_id: &str) -> anyhow::Result<()> {
|
|
let conn = self.conn.lock().map_err(|e| anyhow::anyhow!("store 锁中毒: {e}"))?;
|
|
let now = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as i64)
|
|
.unwrap_or(0);
|
|
conn.execute(
|
|
"INSERT INTO session_map (platform, chat_id, conv_id, owner_user_id, channel_id, created_at, updated_at) \
|
|
VALUES (?1, ?2, ?3, '', ?4, ?5, ?5) \
|
|
ON CONFLICT(platform, chat_id) DO UPDATE SET conv_id = ?3, channel_id = ?4, updated_at = ?5",
|
|
rusqlite::params![platform, chat_id, conv_id, channel_id, now],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// 删除映射(桌面端会话已删,悬空清理)
|
|
pub fn remove(&self, platform: &str, chat_id: &str) -> anyhow::Result<()> {
|
|
let conn = self.conn.lock().map_err(|e| anyhow::anyhow!("store 锁中毒: {e}"))?;
|
|
conn.execute("DELETE FROM session_map WHERE platform = ?1 AND chat_id = ?2", [platform, chat_id])?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn upsert_and_get() {
|
|
let store = SessionStore::open_in_memory().unwrap();
|
|
store.set_conv("feishu", "oc_123", "conv_abc", "chan-work").unwrap();
|
|
let m = store.get_conv("feishu", "oc_123").unwrap();
|
|
assert_eq!(m.conv_id, "conv_abc");
|
|
assert_eq!(m.channel_id, "chan-work");
|
|
assert_eq!(m.owner_user_id, "");
|
|
|
|
// upsert 覆盖
|
|
store.set_conv("feishu", "oc_123", "conv_new", "chan-home").unwrap();
|
|
let m = store.get_conv("feishu", "oc_123").unwrap();
|
|
assert_eq!(m.conv_id, "conv_new");
|
|
|
|
// 删除后悬空
|
|
store.remove("feishu", "oc_123").unwrap();
|
|
assert!(store.get_conv("feishu", "oc_123").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn platform_isolated() {
|
|
let store = SessionStore::open_in_memory().unwrap();
|
|
store.set_conv("feishu", "c1", "conv1", "ch").unwrap();
|
|
store.set_conv("wecom", "c1", "conv2", "ch").unwrap();
|
|
assert_eq!(store.get_conv("feishu", "c1").unwrap().conv_id, "conv1");
|
|
assert_eq!(store.get_conv("wecom", "c1").unwrap().conv_id, "conv2");
|
|
}
|
|
}
|