test(common): ✅ optimize old account content when switching accounts
switch sqlite according to uid
This commit is contained in:
@@ -3,7 +3,6 @@ use crate::command::message_command::MessageResp;
|
||||
use crate::repository::im_message_repository;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use tauri::State;
|
||||
use tracing::{error, info};
|
||||
|
||||
@@ -74,7 +73,7 @@ pub async fn query_chat_history(
|
||||
|
||||
// 查询数据库
|
||||
let messages =
|
||||
im_message_repository::query_chat_history(state.db_conn.deref(), query_condition)
|
||||
im_message_repository::query_chat_history(&*state.db_conn.read().await, query_condition)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("查询聊天历史记录失败: {}", e);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use crate::AppData;
|
||||
use crate::error::CommonError;
|
||||
use crate::im_request_client::{ImRequestClient, ImUrl};
|
||||
use crate::repository::im_contact_repository::{save_contact_batch, update_contact_hide};
|
||||
use crate::repository::im_contact_repository::{
|
||||
list_contact, save_contact_batch, update_contact_hide,
|
||||
};
|
||||
|
||||
use entity::im_contact;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::{error, info};
|
||||
|
||||
#[tauri::command]
|
||||
@@ -24,6 +25,30 @@ pub async fn list_contacts_command(
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
// 先尝试从本地 SQLite 读取(即时返回)
|
||||
let local_data = list_contact(&*state.db_conn.read().await, &login_uid).await;
|
||||
|
||||
if let Ok(local_contacts) = &local_data {
|
||||
if !local_contacts.is_empty() {
|
||||
info!(
|
||||
"Returning {} contacts from local SQLite",
|
||||
local_contacts.len()
|
||||
);
|
||||
// 后台异步更新网络数据
|
||||
let db_conn = state.db_conn.clone();
|
||||
let rc = state.rc.clone();
|
||||
let uid = login_uid.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = fetch_and_update_contacts(db_conn, rc, uid).await {
|
||||
error!("Background contact sync failed: {:?}", e);
|
||||
}
|
||||
});
|
||||
return Ok(local_contacts.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// 本地无数据,从网络获取
|
||||
info!("No local contacts, fetching from network");
|
||||
let data =
|
||||
fetch_and_update_contacts(state.db_conn.clone(), state.rc.clone(), login_uid.clone())
|
||||
.await?;
|
||||
@@ -42,7 +67,7 @@ pub async fn list_contacts_command(
|
||||
|
||||
/// 获取并更新联系人数据
|
||||
async fn fetch_and_update_contacts(
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
db_conn: Arc<RwLock<DatabaseConnection>>,
|
||||
request_client: Arc<Mutex<ImRequestClient>>,
|
||||
login_uid: String,
|
||||
) -> Result<Vec<im_contact::Model>, CommonError> {
|
||||
@@ -58,7 +83,7 @@ async fn fetch_and_update_contacts(
|
||||
|
||||
if let Some(data) = resp {
|
||||
// 保存到本地数据库
|
||||
save_contact_batch(db_conn.deref(), data.clone(), &login_uid)
|
||||
save_contact_batch(&*db_conn.read().await, data.clone(), &login_uid)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
@@ -111,7 +136,7 @@ pub async fn hide_contact_command(
|
||||
if let Some(_) = resp {
|
||||
// 更新本地数据库
|
||||
update_contact_hide(
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
&data.room_id.clone(),
|
||||
data.hide,
|
||||
&login_uid,
|
||||
|
||||
64
src-tauri/src/command/database_command.rs
Normal file
64
src-tauri/src/command/database_command.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use crate::AppData;
|
||||
use crate::configuration::get_configuration;
|
||||
use crate::error::CommonError;
|
||||
use crate::repository::im_message_repository::reset_table_initialization_flags;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use tauri::{AppHandle, State};
|
||||
use tracing::info;
|
||||
|
||||
/// 切换用户数据库
|
||||
/// 根据用户ID切换到对应的数据库文件,如果数据库不存在则创建
|
||||
///
|
||||
/// # 参数
|
||||
/// * `uid` - 用户ID,用于生成用户专属的数据库文件名
|
||||
/// * `state` - 应用状态
|
||||
/// * `app_handle` - Tauri应用句柄
|
||||
///
|
||||
/// # 返回值
|
||||
/// * `Ok(())` - 切换成功
|
||||
/// * `Err(String)` - 切换失败时返回错误信息
|
||||
#[tauri::command]
|
||||
pub async fn switch_user_database(
|
||||
uid: String,
|
||||
state: State<'_, AppData>,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
info!("Switching to user database for uid: {}", uid);
|
||||
|
||||
let result: Result<(), CommonError> = async {
|
||||
// 获取配置
|
||||
let configuration = get_configuration(&app_handle)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load configuration: {}", e))?;
|
||||
|
||||
// 创建新的数据库连接
|
||||
let new_db = configuration
|
||||
.database
|
||||
.connection_string(&app_handle, Some(&uid))
|
||||
.await?;
|
||||
|
||||
// 执行数据库迁移
|
||||
match Migrator::up(&new_db, None).await {
|
||||
Ok(_) => {
|
||||
info!("Database migration completed for user: {}", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Database migration warning for user {}: {}", uid, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表初始化标志,确保新数据库会创建必要的表
|
||||
reset_table_initialization_flags();
|
||||
|
||||
// 替换数据库连接
|
||||
{
|
||||
let mut db_guard = state.db_conn.write().await;
|
||||
*db_guard = new_db;
|
||||
}
|
||||
|
||||
info!("Successfully switched to database for user: {}", uid);
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
result.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -4,7 +4,6 @@ use entity::im_message;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use tauri::State;
|
||||
use tracing::info;
|
||||
|
||||
@@ -96,18 +95,19 @@ pub async fn query_files(
|
||||
};
|
||||
|
||||
// 查询数据库
|
||||
let db = state.db_conn.read().await;
|
||||
let messages = match param.navigation_type.as_str() {
|
||||
"myFiles" => {
|
||||
// 查询所有房间的文件
|
||||
query_all_files(state.db_conn.deref(), &login_uid, ¶m).await?
|
||||
query_all_files(&*db, &login_uid, ¶m).await?
|
||||
}
|
||||
"senders" => {
|
||||
// 按发送人分组查询文件
|
||||
query_files_by_senders(state.db_conn.deref(), &login_uid, ¶m).await?
|
||||
query_files_by_senders(&*db, &login_uid, ¶m).await?
|
||||
}
|
||||
"sessions" | "groups" => {
|
||||
// 按会话或群聊分组查询文件
|
||||
query_files_by_sessions(state.db_conn.deref(), &login_uid, ¶m).await?
|
||||
query_files_by_sessions(&*db, &login_uid, ¶m).await?
|
||||
}
|
||||
_ => {
|
||||
return Err("不支持的导航类型".to_string());
|
||||
@@ -485,10 +485,12 @@ pub async fn debug_message_stats(state: State<'_, AppData>) -> Result<serde_json
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
let db = state.db_conn.read().await;
|
||||
|
||||
// 查询总消息数
|
||||
let total_messages = im_message::Entity::find()
|
||||
.filter(im_message::Column::LoginUid.eq(&login_uid))
|
||||
.count(state.db_conn.deref())
|
||||
.count(&*db)
|
||||
.await
|
||||
.map_err(|e| format!("查询总消息数失败: {}", e))?;
|
||||
|
||||
@@ -504,7 +506,7 @@ pub async fn debug_message_stats(state: State<'_, AppData>) -> Result<serde_json
|
||||
let count = im_message::Entity::find()
|
||||
.filter(im_message::Column::LoginUid.eq(&login_uid))
|
||||
.filter(im_message::Column::MessageType.eq(msg_type))
|
||||
.count(state.db_conn.deref())
|
||||
.count(&*db)
|
||||
.await
|
||||
.map_err(|e| format!("查询类型 {} 消息数失败: {}", msg_type, e))?;
|
||||
|
||||
@@ -522,7 +524,7 @@ pub async fn debug_message_stats(state: State<'_, AppData>) -> Result<serde_json
|
||||
.filter(im_message::Column::MessageType.is_in([4, 6]))
|
||||
.order_by_desc(im_message::Column::SendTime)
|
||||
.limit(5)
|
||||
.all(state.db_conn.deref())
|
||||
.all(&*db)
|
||||
.await
|
||||
.map_err(|e| format!("查询样例消息失败: {}", e))?;
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ use sea_orm::{DatabaseConnection, TransactionTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use tauri::{State, ipc::Channel};
|
||||
@@ -158,7 +157,7 @@ pub async fn page_msg(
|
||||
|
||||
// 从数据库查询消息
|
||||
let db_result = im_message_repository::cursor_page_messages(
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
param.room_id,
|
||||
param.cursor_page_param,
|
||||
&login_uid,
|
||||
@@ -185,7 +184,7 @@ pub async fn page_msg(
|
||||
} else if let Some(send_time) = msg.message.send_time {
|
||||
// 使用统一的 time_block 计算函数
|
||||
resp.time_block = im_message_repository::calculate_time_block(
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
&msg.message.room_id,
|
||||
&msg.message.id,
|
||||
send_time,
|
||||
@@ -493,8 +492,6 @@ pub async fn sync_messages(
|
||||
param: Option<SyncMessagesParam>,
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<(), String> {
|
||||
use std::ops::Deref;
|
||||
|
||||
let async_data = param.as_ref().and_then(|p| p.async_data).unwrap_or(true);
|
||||
let full_sync = param.as_ref().and_then(|p| p.full_sync).unwrap_or(false);
|
||||
let uid = match param.as_ref().and_then(|p| p.uid.clone()) {
|
||||
@@ -505,7 +502,7 @@ pub async fn sync_messages(
|
||||
let mut client = state.rc.lock().await;
|
||||
check_user_init_and_fetch_messages(
|
||||
&mut client,
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
&uid,
|
||||
async_data,
|
||||
full_sync,
|
||||
@@ -596,8 +593,6 @@ pub async fn send_msg(
|
||||
success_channel: Channel<MessageResp>,
|
||||
error_channel: Channel<String>,
|
||||
) -> Result<(), String> {
|
||||
use std::ops::Deref;
|
||||
|
||||
// 获取当前登录用户信息
|
||||
let (login_uid, nickname) = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
@@ -641,7 +636,8 @@ pub async fn send_msg(
|
||||
let db_conn = state.db_conn.clone(); // 克隆数据库连接供异步使用
|
||||
let mut record = message_record.clone(); // 拷贝消息记录以便闭包内可变
|
||||
async move {
|
||||
let tx = db_conn.begin().await.map_err(CommonError::DatabaseError)?; // 开启事务
|
||||
let db = db_conn.read().await;
|
||||
let tx = db.begin().await.map_err(CommonError::DatabaseError)?; // 开启事务
|
||||
record = im_message_repository::save_message(&tx, record).await?; // 保存消息
|
||||
tx.commit().await.map_err(CommonError::DatabaseError)?; // 提交事务
|
||||
Ok(record)
|
||||
@@ -694,7 +690,7 @@ pub async fn send_msg(
|
||||
|
||||
// 更新消息状态
|
||||
let model = im_message_repository::update_message_status(
|
||||
db_conn.deref(),
|
||||
&*db_conn.read().await,
|
||||
record_for_send,
|
||||
status,
|
||||
id,
|
||||
@@ -727,7 +723,8 @@ pub async fn save_msg(data: MessageResp, state: State<'_, AppData>) -> Result<()
|
||||
let db_conn = state.db_conn.clone();
|
||||
let record = record.clone();
|
||||
async move {
|
||||
let tx = db_conn.begin().await?;
|
||||
let db = db_conn.read().await;
|
||||
let tx = db.begin().await?;
|
||||
im_message_repository::save_message(&tx, record).await?;
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
@@ -748,7 +745,7 @@ pub async fn update_message_recall_status(
|
||||
let login_uid = state.user_info.lock().await.uid.clone();
|
||||
|
||||
im_message_repository::update_message_recall_status(
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
&message_id,
|
||||
message_type,
|
||||
&message_body,
|
||||
@@ -770,40 +767,32 @@ pub async fn delete_message(
|
||||
) -> Result<(), String> {
|
||||
let login_uid = state.user_info.lock().await.uid.clone();
|
||||
|
||||
let db = state.db_conn.read().await;
|
||||
let resolved_room_id = if let Some(room) = room_id {
|
||||
room
|
||||
} else {
|
||||
im_message_repository::get_room_id_by_message_id(
|
||||
state.db_conn.deref(),
|
||||
&message_id,
|
||||
&login_uid,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "消息不存在或房间信息缺失".to_string())?
|
||||
im_message_repository::get_room_id_by_message_id(&*db, &message_id, &login_uid)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "消息不存在或房间信息缺失".to_string())?
|
||||
};
|
||||
|
||||
im_message_repository::delete_message_by_id(state.db_conn.deref(), &message_id, &login_uid)
|
||||
im_message_repository::delete_message_by_id(&*db, &message_id, &login_uid)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to delete message {}: {}", message_id, e);
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
im_message_repository::record_deleted_message(
|
||||
state.db_conn.deref(),
|
||||
&message_id,
|
||||
&resolved_room_id,
|
||||
&login_uid,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Failed to record deletion for message {} in room {}: {}",
|
||||
message_id, resolved_room_id, e
|
||||
);
|
||||
e.to_string()
|
||||
})?;
|
||||
im_message_repository::record_deleted_message(&*db, &message_id, &resolved_room_id, &login_uid)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Failed to record deletion for message {} in room {}: {}",
|
||||
message_id, resolved_room_id, e
|
||||
);
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"Deleted message {} for current user {} from local database",
|
||||
@@ -819,40 +808,34 @@ pub async fn delete_room_messages(
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<u64, String> {
|
||||
let login_uid = state.user_info.lock().await.uid.clone();
|
||||
let db = state.db_conn.read().await;
|
||||
|
||||
let last_msg_id =
|
||||
im_message_repository::get_room_max_message_id(state.db_conn.deref(), &room_id, &login_uid)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Failed to query last message id for room {}: {}",
|
||||
room_id, e
|
||||
);
|
||||
e.to_string()
|
||||
})?;
|
||||
let last_msg_id = im_message_repository::get_room_max_message_id(&*db, &room_id, &login_uid)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Failed to query last message id for room {}: {}",
|
||||
room_id, e
|
||||
);
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
let affected_rows =
|
||||
im_message_repository::delete_messages_by_room(state.db_conn.deref(), &room_id, &login_uid)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to delete messages for room {}: {}", room_id, e);
|
||||
e.to_string()
|
||||
})?;
|
||||
let affected_rows = im_message_repository::delete_messages_by_room(&*db, &room_id, &login_uid)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to delete messages for room {}: {}", room_id, e);
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
im_message_repository::record_room_clear(
|
||||
state.db_conn.deref(),
|
||||
&room_id,
|
||||
&login_uid,
|
||||
last_msg_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Failed to record room clear for room {} (user {}): {}",
|
||||
room_id, login_uid, e
|
||||
);
|
||||
e.to_string()
|
||||
})?;
|
||||
im_message_repository::record_room_clear(&*db, &room_id, &login_uid, last_msg_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Failed to record room clear for room {} (user {}): {}",
|
||||
room_id, login_uid, e
|
||||
);
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"Deleted {} messages for room {} (user {})",
|
||||
|
||||
@@ -27,9 +27,10 @@ pub async fn save_message_mark(
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<(), String> {
|
||||
let result: Result<(), CommonError> = async {
|
||||
let db = state.db_conn.read().await;
|
||||
let messages: Vec<im_message::Model> = im_message::Entity::find()
|
||||
.filter(im_message::Column::Id.eq(data.msg_id.clone()))
|
||||
.all(state.db_conn.as_ref())
|
||||
.all(&*db)
|
||||
.await?;
|
||||
|
||||
for message in messages {
|
||||
@@ -49,13 +50,13 @@ pub async fn save_message_mark(
|
||||
|
||||
// 更新数据库
|
||||
im_message::Entity::update(active_message)
|
||||
.exec(state.db_conn.as_ref())
|
||||
.exec(&*db)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// 开启事务保存到数据库
|
||||
let tx = state.db_conn.begin().await?;
|
||||
let tx = db.begin().await?;
|
||||
// im_message_mark_repository::save_msg_mark(&tx, message_mark).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod ai_command;
|
||||
pub mod app_state_command;
|
||||
pub mod chat_history_command;
|
||||
pub mod contact_command;
|
||||
pub mod database_command;
|
||||
pub mod file_manager_command;
|
||||
pub mod markdown_command;
|
||||
pub mod message_command;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use std::ops::Deref;
|
||||
use tauri::{Emitter, State};
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
AppData,
|
||||
command::message_command::check_user_init_and_fetch_messages,
|
||||
configuration::get_configuration,
|
||||
im_request_client::{ImRequest, ImUrl},
|
||||
repository::im_user_repository,
|
||||
repository::{im_message_repository::reset_table_initialization_flags, im_user_repository},
|
||||
vo::vo::{LoginReq, LoginResp, RefreshTokenReq},
|
||||
};
|
||||
|
||||
@@ -14,14 +15,18 @@ use crate::{
|
||||
pub async fn login_command(
|
||||
data: LoginReq,
|
||||
state: State<'_, AppData>,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<Option<LoginResp>, String> {
|
||||
if data.is_auto_login {
|
||||
// 自动登录逻辑
|
||||
if let Some(uid) = &data.uid {
|
||||
info!("Attempting auto login, user ID: {}", uid);
|
||||
|
||||
// 先切换到用户专属数据库
|
||||
switch_to_user_database(&state, &app_handle, uid).await?;
|
||||
|
||||
// 从数据库获取用户的 refresh_token
|
||||
match im_user_repository::get_user_tokens(state.db_conn.deref(), uid).await {
|
||||
match im_user_repository::get_user_tokens(&*state.db_conn.read().await, uid).await {
|
||||
Ok(Some((_, refresh_token))) => {
|
||||
info!(
|
||||
"Found refresh_token for user {}, attempting to refresh login",
|
||||
@@ -44,7 +49,7 @@ pub async fn login_command(
|
||||
|
||||
// 保存新的 token 信息到数据库
|
||||
if let Err(e) = im_user_repository::save_user_tokens(
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
uid,
|
||||
&refresh_resp.token,
|
||||
&refresh_resp.refresh_token,
|
||||
@@ -99,6 +104,8 @@ pub async fn login_command(
|
||||
|
||||
// 登录成功后处理用户信息和token保存
|
||||
if let Some(login_resp) = &res {
|
||||
// 先切换到用户专属数据库
|
||||
switch_to_user_database(&state, &app_handle, &login_resp.uid).await?;
|
||||
handle_login_success(login_resp, &state, async_data).await?;
|
||||
}
|
||||
|
||||
@@ -107,6 +114,48 @@ pub async fn login_command(
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换到用户专属数据库
|
||||
async fn switch_to_user_database(
|
||||
state: &State<'_, AppData>,
|
||||
app_handle: &AppHandle,
|
||||
uid: &str,
|
||||
) -> Result<(), String> {
|
||||
info!("Switching to user database for uid: {}", uid);
|
||||
|
||||
// 获取配置
|
||||
let configuration = get_configuration(app_handle)
|
||||
.map_err(|e| format!("Failed to load configuration: {}", e))?;
|
||||
|
||||
// 创建新的数据库连接
|
||||
let new_db = configuration
|
||||
.database
|
||||
.connection_string(app_handle, Some(uid))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 执行数据库迁移
|
||||
match Migrator::up(&new_db, None).await {
|
||||
Ok(_) => {
|
||||
info!("Database migration completed for user: {}", uid);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Database migration warning for user {}: {}", uid, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表初始化标志,确保新数据库会创建必要的表
|
||||
reset_table_initialization_flags();
|
||||
|
||||
// 替换数据库连接
|
||||
{
|
||||
let mut db_guard = state.db_conn.write().await;
|
||||
*db_guard = new_db;
|
||||
}
|
||||
|
||||
info!("Successfully switched to database for user: {}", uid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_login_success(
|
||||
login_resp: &LoginResp,
|
||||
state: &State<'_, AppData>,
|
||||
@@ -124,7 +173,7 @@ async fn handle_login_success(
|
||||
info!("handle_login_success, user_info: {:?}", user_info);
|
||||
// 保存 token 信息到数据库
|
||||
im_user_repository::save_user_tokens(
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
uid,
|
||||
&login_resp.token,
|
||||
&login_resp.refresh_token,
|
||||
@@ -133,9 +182,15 @@ async fn handle_login_success(
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut client = state.rc.lock().await;
|
||||
check_user_init_and_fetch_messages(&mut client, state.db_conn.deref(), uid, async_data, false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
check_user_init_and_fetch_messages(
|
||||
&mut client,
|
||||
&*state.db_conn.read().await,
|
||||
uid,
|
||||
async_data,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9,10 +9,8 @@ use tracing::{error, info};
|
||||
|
||||
use crate::im_request_client::{ImRequestClient, ImUrl};
|
||||
use crate::repository::im_room_member_repository;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -67,7 +65,7 @@ pub async fn update_my_room_info(
|
||||
|
||||
// 更新本地数据库
|
||||
update_my_room_info_db(
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
&my_room_info.my_name,
|
||||
&my_room_info.id,
|
||||
&uid,
|
||||
@@ -103,18 +101,7 @@ pub async fn get_room_members(
|
||||
) -> Result<Vec<RoomMemberResponse>, String> {
|
||||
info!("Calling to get all member list of room with room_id");
|
||||
let result: Result<Vec<RoomMemberResponse>, CommonError> = async {
|
||||
let login_uid = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
let mut members = fetch_and_update_room_members(
|
||||
room_id.clone(),
|
||||
state.db_conn.clone(),
|
||||
state.rc.clone(),
|
||||
login_uid.clone(),
|
||||
)
|
||||
.await?;
|
||||
let mut members = fetch_and_update_room_members(room_id.clone(), state.rc.clone()).await?;
|
||||
|
||||
sort_room_members(&mut members);
|
||||
|
||||
@@ -152,7 +139,7 @@ pub async fn cursor_page_room_members(
|
||||
};
|
||||
|
||||
let data = im_room_member_repository::cursor_page_room_members(
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
param.room_id,
|
||||
param.cursor_page_param,
|
||||
&login_uid,
|
||||
@@ -235,9 +222,7 @@ fn sort_room_members(members: &mut Vec<RoomMemberResponse>) {
|
||||
/// 异步更新房间成员数据
|
||||
async fn fetch_and_update_room_members(
|
||||
room_id: String,
|
||||
_db_conn: Arc<DatabaseConnection>,
|
||||
request_client: Arc<Mutex<ImRequestClient>>,
|
||||
_login_uid: String,
|
||||
) -> Result<Vec<RoomMemberResponse>, CommonError> {
|
||||
let resp: Option<Vec<RoomMemberResponse>> = request_client
|
||||
.lock()
|
||||
|
||||
@@ -9,7 +9,6 @@ use sea_orm::EntityTrait;
|
||||
use sea_orm::IntoActiveModel;
|
||||
use sea_orm::QueryFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use tauri::State;
|
||||
use tracing::{debug, info};
|
||||
|
||||
@@ -46,12 +45,12 @@ pub async fn save_user_info(
|
||||
user_info: SaveUserInfoRequest,
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<(), String> {
|
||||
let db = state.db_conn.clone();
|
||||
let db = state.db_conn.read().await;
|
||||
|
||||
// 检查用户是否存在
|
||||
let exists = ImUserEntity::find()
|
||||
.filter(im_user::Column::Id.eq(&user_info.uid))
|
||||
.one(db.deref())
|
||||
.one(&*db)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to query user: {}", err))?;
|
||||
|
||||
@@ -66,7 +65,7 @@ pub async fn save_user_info(
|
||||
};
|
||||
|
||||
im_user::Entity::insert(user)
|
||||
.exec(db.deref())
|
||||
.exec(&*db)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to insert user: {}", err))?;
|
||||
} else {
|
||||
@@ -78,14 +77,14 @@ pub async fn save_user_info(
|
||||
#[tauri::command]
|
||||
pub async fn update_user_last_opt_time(state: State<'_, AppData>) -> Result<(), String> {
|
||||
info!("Updating user last operation time");
|
||||
let db = state.db_conn.clone();
|
||||
let db = state.db_conn.read().await;
|
||||
|
||||
let uid = state.user_info.lock().await.uid.clone();
|
||||
|
||||
// 检查用户是否存在
|
||||
let user = ImUserEntity::find()
|
||||
.filter(im_user::Column::Id.eq(uid.clone()))
|
||||
.one(db.deref())
|
||||
.one(&*db)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to query user: {}", err))?;
|
||||
|
||||
@@ -94,7 +93,7 @@ pub async fn update_user_last_opt_time(state: State<'_, AppData>) -> Result<(),
|
||||
active_model.last_opt_time = Set(Some(Local::now().timestamp_millis()));
|
||||
|
||||
ImUserEntity::update(active_model)
|
||||
.exec(db.deref())
|
||||
.exec(&*db)
|
||||
.await
|
||||
.map_err(|err| format!("Failed to update user last operation time: {}", err))?;
|
||||
}
|
||||
@@ -149,7 +148,7 @@ pub async fn update_token(
|
||||
rc.refresh_token = Some(req.refresh_token.clone());
|
||||
}
|
||||
im_user_repository::save_user_tokens(
|
||||
state.db_conn.deref(),
|
||||
&*state.db_conn.read().await,
|
||||
&req.uid,
|
||||
&req.token,
|
||||
&req.refresh_token,
|
||||
|
||||
@@ -68,12 +68,23 @@ pub enum Environment {
|
||||
}
|
||||
|
||||
impl DatabaseSettings {
|
||||
/// 根据用户ID生成数据库文件名
|
||||
/// 如果提供了用户ID,则生成 `db_{uid}.sqlite` 格式的文件名
|
||||
/// 否则使用默认的 `db.sqlite`
|
||||
fn get_db_filename(uid: Option<&str>) -> String {
|
||||
match uid {
|
||||
Some(id) if !id.is_empty() => format!("db_{}.sqlite", id),
|
||||
_ => "db.sqlite".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建数据库连接
|
||||
/// 根据不同的运行环境(桌面开发、移动端、桌面生产)选择合适的数据库路径
|
||||
/// 并配置数据库连接选项,返回数据库连接实例
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app_handle` - Tauri应用句柄,用于获取应用路径
|
||||
/// * `uid` - 可选的用户ID,用于生成用户专属的数据库文件
|
||||
///
|
||||
/// # 返回值
|
||||
/// * `Ok(DatabaseConnection)` - 成功时返回数据库连接
|
||||
@@ -81,12 +92,16 @@ impl DatabaseSettings {
|
||||
pub async fn connection_string(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
uid: Option<&str>,
|
||||
) -> Result<DatabaseConnection, CommonError> {
|
||||
let db_filename = Self::get_db_filename(uid);
|
||||
info!("Database filename: {}", db_filename);
|
||||
|
||||
// 数据库路径配置:
|
||||
let db_path = if cfg!(debug_assertions) && cfg!(desktop) {
|
||||
// 桌面端开发环境:使用项目根目录(src-tauri)
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("db.sqlite");
|
||||
path.push(&db_filename);
|
||||
path
|
||||
} else {
|
||||
match app_handle.path().app_data_dir() {
|
||||
@@ -94,7 +109,7 @@ impl DatabaseSettings {
|
||||
if let Err(create_err) = std::fs::create_dir_all(&app_data_dir) {
|
||||
tracing::warn!("Failed to create app_data_dir: {}", create_err);
|
||||
}
|
||||
let db_path = app_data_dir.join("db.sqlite");
|
||||
let db_path = app_data_dir.join(&db_filename);
|
||||
info!("Using app_data_dir database path: {:?}", db_path);
|
||||
db_path
|
||||
}
|
||||
|
||||
@@ -1036,7 +1036,6 @@ impl ImUrl {
|
||||
|
||||
// ================ 平台配置 ================
|
||||
"platformList" => Ok(ImUrl::PlatformList),
|
||||
"modelRemainingUsage" => Ok(ImUrl::ModelRemainingUsage),
|
||||
"platformAddModel" => Ok(ImUrl::PlatformAddModel),
|
||||
|
||||
// ================ AI 工具 ================
|
||||
|
||||
@@ -61,7 +61,7 @@ use mobiles::splash;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppData {
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
db_conn: Arc<RwLock<DatabaseConnection>>,
|
||||
user_info: Arc<Mutex<UserInfo>>,
|
||||
pub rc: Arc<Mutex<im_request_client::ImRequestClient>>,
|
||||
pub config: Arc<Mutex<Settings>>,
|
||||
@@ -77,6 +77,7 @@ pub(crate) static APP_STATE_READY: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
use crate::command::chat_history_command::query_chat_history;
|
||||
use crate::command::contact_command::{hide_contact_command, list_contacts_command};
|
||||
use crate::command::database_command::switch_user_database;
|
||||
use crate::command::file_manager_command::{
|
||||
debug_message_stats, get_navigation_items, query_files,
|
||||
};
|
||||
@@ -91,7 +92,7 @@ use crate::command::oauth_command::start_oauth_server;
|
||||
#[cfg(desktop)]
|
||||
use tauri::Listener;
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
pub fn run() {
|
||||
#[cfg(desktop)]
|
||||
@@ -144,7 +145,7 @@ async fn initialize_app_data(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<
|
||||
(
|
||||
Arc<DatabaseConnection>,
|
||||
Arc<RwLock<DatabaseConnection>>,
|
||||
Arc<Mutex<UserInfo>>,
|
||||
Arc<Mutex<im_request_client::ImRequestClient>>,
|
||||
Arc<Mutex<Settings>>,
|
||||
@@ -161,17 +162,17 @@ async fn initialize_app_data(
|
||||
})?));
|
||||
|
||||
// 初始化数据库连接
|
||||
let db: Arc<DatabaseConnection> = Arc::new(
|
||||
let db: Arc<RwLock<DatabaseConnection>> = Arc::new(RwLock::new(
|
||||
configuration
|
||||
.lock()
|
||||
.await
|
||||
.database
|
||||
.connection_string(&app_handle)
|
||||
.connection_string(&app_handle, None)
|
||||
.await?,
|
||||
);
|
||||
));
|
||||
|
||||
// 数据库迁移
|
||||
match Migrator::up(db.as_ref(), None).await {
|
||||
match Migrator::up(&*db.read().await, None).await {
|
||||
Ok(_) => {
|
||||
info!("Database migration completed");
|
||||
}
|
||||
@@ -496,5 +497,6 @@ fn get_invoke_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Se
|
||||
#[cfg(target_os = "ios")]
|
||||
set_webview_keyboard_adjustment,
|
||||
is_app_state_ready,
|
||||
switch_user_database,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,6 +20,13 @@ lazy_static! {
|
||||
static ref ROOM_CLEAR_TABLE_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
/// 重置表初始化标志,在切换数据库时调用
|
||||
pub fn reset_table_initialization_flags() {
|
||||
DELETED_TABLE_INITIALIZED.store(false, Ordering::SeqCst);
|
||||
ROOM_CLEAR_TABLE_INITIALIZED.store(false, Ordering::SeqCst);
|
||||
info!("Table initialization flags have been reset");
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MessageWithThumbnail {
|
||||
pub message: im_message::Model,
|
||||
|
||||
@@ -118,19 +118,19 @@
|
||||
<!-- Gitee/GitHub/GitCode 标识 -->
|
||||
<n-tooltip v-if="linkedGitee">
|
||||
<template #trigger>
|
||||
<svg class="size-18px dark:color-#d5304f"><use href="#gitee-login"></use></svg>
|
||||
<svg class="size-18px color-#d5304f"><use href="#gitee-login"></use></svg>
|
||||
</template>
|
||||
<span>{{ t('home.profile_card.tooltip.bound_gitee') }}</span>
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="linkedGithub">
|
||||
<template #trigger>
|
||||
<svg class="size-18px dark:color-#fefefe"><use href="#github-login"></use></svg>
|
||||
<svg class="size-18px color-#303030 dark:color-#fefefe"><use href="#github-login"></use></svg>
|
||||
</template>
|
||||
<span>{{ t('home.profile_card.tooltip.bound_github') }}</span>
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="linkedGitcode">
|
||||
<template #trigger>
|
||||
<svg class="size-18px dark:color-#d5304f"><use href="#gitcode-login"></use></svg>
|
||||
<svg class="size-18px color-#d5304f"><use href="#gitcode-login"></use></svg>
|
||||
</template>
|
||||
<span>{{ t('home.profile_card.tooltip.bound_gitcode') }}</span>
|
||||
</n-tooltip>
|
||||
|
||||
@@ -127,6 +127,9 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
const commonTheme: GlobalThemeOverrides = {
|
||||
Badge: {
|
||||
color: '#c14053'
|
||||
},
|
||||
Input: {
|
||||
borderRadius: '10px',
|
||||
borderHover: '0',
|
||||
|
||||
@@ -49,6 +49,34 @@ export const useLogin = () => {
|
||||
|
||||
const { t } = useI18nGlobal()
|
||||
|
||||
/**
|
||||
* 清空 localStorage 中用户相关的持久化数据
|
||||
* 防止 Pinia 在页面刷新时自动恢复旧账号数据
|
||||
*/
|
||||
const clearUserLocalStorage = () => {
|
||||
const userScopedStoreKeys = ['chat', 'group', 'contacts', 'feed', 'cached', 'sessionUnread']
|
||||
userScopedStoreKeys.forEach((key) => {
|
||||
localStorage.removeItem(key)
|
||||
})
|
||||
console.log('[useLogin] User localStorage has been cleared')
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空消息缓存和群组数据
|
||||
* 在新数据加载完成后调用,避免旧消息混入
|
||||
*/
|
||||
const clearMessageCache = () => {
|
||||
// 清空消息缓存(messageMap 是 reactive Record,需要逐个删除键)
|
||||
for (const key of Object.keys(chatStore.messageMap)) {
|
||||
delete chatStore.messageMap[key]
|
||||
}
|
||||
// 清空群组成员数据
|
||||
for (const key of Object.keys(groupStore.userListMap)) {
|
||||
delete groupStore.userListMap[key]
|
||||
}
|
||||
console.log('[useLogin] Message cache has been cleared')
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 composable 初始化时获取 router 实例
|
||||
* 注意: useRouter() 必须在组件 setup 上下文中调用
|
||||
@@ -96,6 +124,7 @@ export const useLogin = () => {
|
||||
*/
|
||||
const logout = async () => {
|
||||
globalStore.updateCurrentSessionRoomId('')
|
||||
|
||||
const sendLogoutEvent = async () => {
|
||||
// ws 退出连接
|
||||
await invokeSilently('ws_disconnect')
|
||||
@@ -210,11 +239,29 @@ export const useLogin = () => {
|
||||
|
||||
const init = async (options?: { isInitialSync?: boolean }) => {
|
||||
const emojiStore = useEmojiStore()
|
||||
// 保存当前选中的会话,避免同步时丢失用户的选中状态
|
||||
|
||||
// 保存当前选中的会话,同步后如果该会话仍存在则恢复选中状态
|
||||
const previousSessionRoomId = globalStore.currentSessionRoomId
|
||||
|
||||
// 清空 localStorage,防止页面刷新时恢复旧账号数据
|
||||
clearUserLocalStorage()
|
||||
|
||||
// 清空消息缓存,避免旧消息混入新账号
|
||||
clearMessageCache()
|
||||
|
||||
// 立即清空旧账号的会话列表,并立即获取新账号数据
|
||||
// 这样用户看到的是短暂加载而不是错误的旧数据
|
||||
chatStore.sessionList.length = 0
|
||||
groupStore.groupDetails.length = 0
|
||||
|
||||
// 连接 ws
|
||||
await rustWebSocketClient.initConnect()
|
||||
|
||||
// 立即获取新账号的会话列表(优先加载,减少空白时间)
|
||||
chatStore.getSessionList(true).catch(() => {
|
||||
void logInfo('[useLogin] 获取会话列表失败')
|
||||
})
|
||||
|
||||
// 用户相关数据初始化
|
||||
userStatusStore.stateList = await getAllUserState()
|
||||
const userDetail: any = await getUserDetail()
|
||||
@@ -357,6 +404,7 @@ export const useLogin = () => {
|
||||
}
|
||||
})
|
||||
.then(async (_: any) => {
|
||||
// 数据库切换已在后端 login_command 中完成
|
||||
loginDisabled.value = true
|
||||
loading.value = false
|
||||
loginText.value = t('login.status.success_redirect')
|
||||
@@ -432,6 +480,10 @@ export const useLogin = () => {
|
||||
throw new Error('授权回调缺少 token 或 refreshToken')
|
||||
}
|
||||
const targetUid = uid || undefined
|
||||
// 先切换到用户专属数据库
|
||||
if (targetUid) {
|
||||
await invoke('switch_user_database', { uid: targetUid })
|
||||
}
|
||||
await TokenManager.updateToken(token, refreshToken, targetUid)
|
||||
await invoke('sync_messages', {
|
||||
param: {
|
||||
@@ -546,6 +598,10 @@ export const useLogin = () => {
|
||||
throw new Error('授权回调缺少 token 或 refreshToken')
|
||||
}
|
||||
const targetUid = uid || undefined
|
||||
// 先切换到用户专属数据库
|
||||
if (targetUid) {
|
||||
await invoke('switch_user_database', { uid: targetUid })
|
||||
}
|
||||
await TokenManager.updateToken(token, refreshToken, targetUid)
|
||||
await invoke('sync_messages', {
|
||||
param: {
|
||||
@@ -635,6 +691,10 @@ export const useLogin = () => {
|
||||
throw new Error('授权回调缺少 token 或 refreshToken')
|
||||
}
|
||||
const targetUid = uid || undefined
|
||||
// 先切换到用户专属数据库
|
||||
if (targetUid) {
|
||||
await invoke('switch_user_database', { uid: targetUid })
|
||||
}
|
||||
await TokenManager.updateToken(token, refreshToken, targetUid)
|
||||
await invoke('sync_messages', {
|
||||
param: {
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<div class="flex-shrink-0">
|
||||
<n-badge
|
||||
:offset="[-6, 6]"
|
||||
:color="item.muteNotification === NotificationTypeEnum.NOT_DISTURB ? 'grey' : '#d5304f'"
|
||||
:color="item.muteNotification === NotificationTypeEnum.NOT_DISTURB ? 'grey' : '#c14053'"
|
||||
:value="item.unreadCount"
|
||||
:max="99">
|
||||
<n-avatar :size="52" :src="AvatarUtils.getAvatarUrl(item.avatar)" fallback-src="/logo.png" round />
|
||||
|
||||
@@ -59,6 +59,16 @@ export const updateSettings = async (settings: UpdateSettingsParams) => {
|
||||
return await invoke('update_settings', { settings })
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换用户数据库
|
||||
* 根据用户ID切换到对应的数据库文件,如果数据库不存在则创建
|
||||
* @param uid 用户ID
|
||||
*/
|
||||
export const switchUserDatabase = async (uid: string): Promise<void> => {
|
||||
await ensureAppStateReady()
|
||||
return await invoke('switch_user_database', { uid })
|
||||
}
|
||||
|
||||
export const loginCommand = async (
|
||||
info: Partial<{
|
||||
account: string
|
||||
@@ -91,6 +101,7 @@ export const loginCommand = async (
|
||||
uid: info.uid
|
||||
}
|
||||
}).then(async (res: any) => {
|
||||
// 数据库切换已在后端 login_command 中完成
|
||||
// 开启 ws 连接
|
||||
await rustWebSocketClient.initConnect()
|
||||
await loginProcess(res.token, res.refreshToken, res.client)
|
||||
|
||||
@@ -35,7 +35,7 @@ export const useSettingStore = defineStore(StoresEnum.SETTING, {
|
||||
state: (): STO.Setting => ({
|
||||
themes: {
|
||||
content: '',
|
||||
pattern: '',
|
||||
pattern: ThemeEnum.OS,
|
||||
versatile: isDesktopComputed.value ? 'default' : 'simple'
|
||||
},
|
||||
escClose: true,
|
||||
|
||||
Reference in New Issue
Block a user