test(common): optimize old account content when switching accounts

switch sqlite according to uid
This commit is contained in:
Dawn
2026-01-09 22:01:24 +08:00
parent ca10f5ddce
commit d1812f397d
20 changed files with 346 additions and 135 deletions

View File

@@ -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);

View File

@@ -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,

View 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())
}

View File

@@ -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, &param).await?
query_all_files(&*db, &login_uid, &param).await?
}
"senders" => {
// 按发送人分组查询文件
query_files_by_senders(state.db_conn.deref(), &login_uid, &param).await?
query_files_by_senders(&*db, &login_uid, &param).await?
}
"sessions" | "groups" => {
// 按会话或群聊分组查询文件
query_files_by_sessions(state.db_conn.deref(), &login_uid, &param).await?
query_files_by_sessions(&*db, &login_uid, &param).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))?;

View File

@@ -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 {})",

View File

@@ -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?;

View File

@@ -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;

View File

@@ -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(())
}

View File

@@ -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()

View File

@@ -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,

View File

@@ -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
}

View File

@@ -1036,7 +1036,6 @@ impl ImUrl {
// ================ 平台配置 ================
"platformList" => Ok(ImUrl::PlatformList),
"modelRemainingUsage" => Ok(ImUrl::ModelRemainingUsage),
"platformAddModel" => Ok(ImUrl::PlatformAddModel),
// ================ AI 工具 ================

View File

@@ -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,
]
}

View File

@@ -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,

View File

@@ -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>

View File

@@ -127,6 +127,9 @@ onUnmounted(() => {
})
const commonTheme: GlobalThemeOverrides = {
Badge: {
color: '#c14053'
},
Input: {
borderRadius: '10px',
borderHover: '0',

View File

@@ -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: {

View File

@@ -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 />

View File

@@ -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)

View File

@@ -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,