mirror of
https://github.com/HuLaSpark/HuLa.git
synced 2026-09-01 14:52:48 +08:00
feat(common): ✨ sqlite 本地存储 (#305)
* feat(store): ✨ rust集成 sqlite * chore(config): 🔨 移除一些不必要的文件 * feat(common): ✨ 群组信息 command * feat(common): ✨ rust 重构 * feat(common): ✨ tauri 登录命令 存储 token * feat(common): ✨ tauri 存储 room 信息 * feat(common): ✨ sqlite 存储房间信息,群成员信息 * feat(common): ✨ 修改我的群的信息 * feat(common): ✨ 游标分页查询群组成员 * feat(common): ✨ 查询全部群成员;优化前端显示逻辑 * feat(common): ✨ 更新 gitignore * feat(common): ✨ 使用缓存检测是否强制更新本地数据,或采取静默更新 * fix(hook): 🐛 枚举使用错误修复 * feat(common): ✨ sqlite 存储会话 * feat(common): ✨ sqlite 存储聊天信息 * fix(common): 🐛 sqlite需要根据登录的用户 id 进行数据隔离 * fix(common): 🐛 添加 sqlite数据库迁移代码 * fix(view): 🐛 会话接口新增detailId;sqlite 数据结构变更;登录保存用户 id bug 修复; * feat(common): ✨ sqlite 存储消息 * fix(common): 🐛 sqlite 存储消息体异常 * feat(common): ✨ 添加 tauri 日志插件 * feat(common): ✨ rust 层发送消息 * fix(component): 🐛 群成员监听显示问题;tauri 事件监听响应多次问题;消息列表显示顺序问题; * fix(common): 🐛 im_message 表关闭 nickname 非空限制 * fix(common): 🐛 设置账号第一次在客户端登录,设置is_init 为 true,进行初始化获取近期所有消息 * fix(common): 🐛 配置 tauri 的环境变量,用于 migration 启动 * fix(common): 🐛 登录之后,聊天页面初始化,未显示聊天记录 bug * feat(system): ✨ 合并master * style(common): 💄 引入 tauri websocket; 格式化 rust 代码; * fix(common): 🐛 ws 接收消息存储本地 * fix(common): 🐛 rust 报错没有在前端提示问题修复 * fix(common): 🐛 修复登录页面请求接口 bug,以及首屏骨架屏问题; * fix(common): 🐛 token 更新bug修复 * feat(common): ✨ 离线消息同步 * fix(common): 🐛 将数据库迁移 crate 集成到主进程中 * style(common): 💄 修改 tauri 日志配置,输出日志到 logs 目录下;并对日志级别添加颜色; * feat(common): ✨ 引入 tauri-devtools * docs(common): 📝 env文件修改 * fix(common): 🐛 rust 依赖调整;配置调整; * build(config): 📦 修改配置 --------- Co-authored-by: Dawn <2439646234@qq.com>
This commit is contained in:
+2
-2
@@ -10,5 +10,5 @@ VITE_APP_NAME="HuLa"
|
||||
VITE_GITEE_TOKEN="a9029798336825cea39ac9e4413b8579"
|
||||
|
||||
# 启用本地的服务地址,先要注释掉上面的服务地址
|
||||
# VITE_SERVICE_URL="http://127.0.0.1:9190"
|
||||
# VITE_WEBSOCKET_URL="ws://127.0.0.1:8090/websocket"
|
||||
# VITE_SERVICE_URL="http://192.168.1.24:9190"
|
||||
# VITE_WEBSOCKET_URL="ws://192.168.1.24:8090/websocket"
|
||||
|
||||
+6
-1
@@ -27,4 +27,9 @@ src-tauri/target
|
||||
|
||||
/coverage
|
||||
.cursorrules
|
||||
.claude/
|
||||
.claude/
|
||||
|
||||
target
|
||||
.claude/
|
||||
*.sqlite
|
||||
db.sqlite
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"@tauri-apps/plugin-dialog": "^2.3.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||
"@tauri-apps/plugin-http": "2.5.0",
|
||||
"@tauri-apps/plugin-log": "^2.6.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.0",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": "2.3.0",
|
||||
|
||||
Generated
+10
@@ -35,6 +35,9 @@ importers:
|
||||
'@tauri-apps/plugin-http':
|
||||
specifier: 2.5.0
|
||||
version: 2.5.0
|
||||
'@tauri-apps/plugin-log':
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
'@tauri-apps/plugin-notification':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
@@ -1832,6 +1835,9 @@ packages:
|
||||
'@tauri-apps/plugin-http@2.5.0':
|
||||
resolution: {integrity: sha512-l4M2DUIsOBIMrbj4dJZwrB4mJiB7OA/2Tj3gEbX2fjq5MOpETklJPKfDvzUTDwuq4lIKCKKykz8E8tpOgvi0EQ==}
|
||||
|
||||
'@tauri-apps/plugin-log@2.6.0':
|
||||
resolution: {integrity: sha512-gVp3l31akA1Jk2bZsTA0hMFD5/gLe49Nw1btu5lViau0QqgC2XyT79LSwvy7a44ewtQbSexchqIg7oTJKMIbXQ==}
|
||||
|
||||
'@tauri-apps/plugin-notification@2.3.0':
|
||||
resolution: {integrity: sha512-QDwXo9VzAlH97c0veuf19TZI6cRBPfJDl2O6hNEDvI66j60lOO9z+PL6MJrj8A6Y+t55r7mGhe3rQWLmOre2HA==}
|
||||
|
||||
@@ -6779,6 +6785,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.6.0
|
||||
|
||||
'@tauri-apps/plugin-log@2.6.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.6.0
|
||||
|
||||
'@tauri-apps/plugin-notification@2.3.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.6.0
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
RUST_BACKTRACE=1
|
||||
DATABASE_URL=sqlite:../db.sqlite?mode=rwc
|
||||
Generated
+1292
-112
File diff suppressed because it is too large
Load Diff
+31
-13
@@ -7,6 +7,13 @@ license = ""
|
||||
repository = ""
|
||||
edition = "2024"
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = [".", "entity", "migration"]
|
||||
|
||||
[workspace.dependencies]
|
||||
entity = { path = "entity" }
|
||||
|
||||
[profile.release]
|
||||
panic = "abort" # 去掉昂贵的恐慌清理逻辑
|
||||
codegen-units = 1 # 一个接一个地编译包,这样编译器就可以更好地优化
|
||||
@@ -56,17 +63,24 @@ tauri-plugin-mic-recorder = "2"
|
||||
serde_json = "1"
|
||||
sysinfo = "0.32.0"
|
||||
async-walkdir = "2.0.0"
|
||||
|
||||
# 不兼容移动端的依赖
|
||||
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
||||
tauri-plugin-autostart = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
mime_guess = "2.0.5"
|
||||
|
||||
moka = { version = "0.12.10", features = ["future"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
tauri-plugin-devtools = "2.0.0"
|
||||
|
||||
sea-orm = { version = "1.1.0", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }
|
||||
chrono = "0.4.41"
|
||||
|
||||
entity = { path = "entity" }
|
||||
migration = { path = "migration" } # depends on your needs
|
||||
anyhow = "1.0.98"
|
||||
thiserror = "2.0.12"
|
||||
config = { version = "0.15.11", default-features = false, features = ["yaml"] }
|
||||
http = "0.2"
|
||||
tauri-plugin-log = { version = "2.6.0", features = ["colored"] }
|
||||
log = "0.4" # 基础日志接口
|
||||
lazy_static = "1.4"
|
||||
screenshots = "0.8.10"
|
||||
mime_guess = "2.0.5"
|
||||
base64 = "0.22.1"
|
||||
rodio = "0.17.3"
|
||||
image = { version = "0.25", features = ["jpeg", "png"] }
|
||||
@@ -76,11 +90,15 @@ reqwest = { version = "0.11", features = [
|
||||
"rustls-tls",
|
||||
"blocking",
|
||||
] }
|
||||
tungstenite = { version = "0.26.2", features = ["rustls-tls-webpki-roots"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures = "0.3"
|
||||
surge-ping = "0.8.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
# 不兼容移动端的依赖
|
||||
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
||||
tauri-plugin-autostart = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
screenshots = "0.8.10"
|
||||
|
||||
|
||||
# mac平台需要依赖
|
||||
[target."cfg(target_os =\"macos\")".dependencies]
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
"opener:allow-reveal-item-in-dir",
|
||||
"opener:allow-open-path",
|
||||
"mic-recorder:default",
|
||||
"log:default",
|
||||
{
|
||||
"identifier": "shell:allow-execute",
|
||||
"allow": [
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
database:
|
||||
sqlite_file: db.sqlite
|
||||
backend:
|
||||
base_url: https://hulaspark.com/api
|
||||
@@ -0,0 +1,7 @@
|
||||
# application:
|
||||
# host: 127.0.0.1
|
||||
# base_url: "http://127.0.0.1"
|
||||
# database:
|
||||
# require_ssl: false
|
||||
# backend:
|
||||
# base_url: http://localhost:9190
|
||||
@@ -0,0 +1,4 @@
|
||||
# application:
|
||||
# host: 127.0.0.1
|
||||
# backend:
|
||||
# base_url: http://localhost:9190
|
||||
Generated
+2389
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "entity"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sea-orm = { version = "1.1.0" }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
@@ -0,0 +1,22 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "im_config")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub config_key: String,
|
||||
pub config_value: Option<String>,
|
||||
#[serde(skip)]
|
||||
#[sea_orm(primary_key)]
|
||||
pub login_uid: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,42 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "im_contact")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: String,
|
||||
pub detail_id: String,
|
||||
pub room_id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub contact_type: Option<u8>,
|
||||
pub hot_flag: Option<u8>,
|
||||
pub top: Option<bool>,
|
||||
pub account: Option<String>,
|
||||
pub operate: Option<u8>,
|
||||
pub remark: Option<String>,
|
||||
pub my_name: Option<String>,
|
||||
// "通知类型 0 -> 允许接受消息 1 -> 接收但不提醒[免打扰] 4 -> 已退出群聊"
|
||||
pub mute_notification: Option<u8>,
|
||||
// 删除会话
|
||||
pub hide: Option<bool>,
|
||||
pub active_time: Option<i64>,
|
||||
// true->屏蔽 false -> 正常
|
||||
pub shield: Option<bool>,
|
||||
pub avatar: Option<String>,
|
||||
#[serde(rename = "name")]
|
||||
pub contact_name: Option<String>,
|
||||
pub text: Option<String>,
|
||||
pub unread_count: Option<u8>,
|
||||
pub create_time: Option<i64>,
|
||||
pub update_time: Option<i64>,
|
||||
#[serde(skip)]
|
||||
#[sea_orm(primary_key)]
|
||||
pub login_uid: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,30 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "im_message")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: String,
|
||||
pub uid: String,
|
||||
pub nickname: Option<String>,
|
||||
pub room_id: String,
|
||||
pub send_time: Option<i64>,
|
||||
#[serde(rename = "type")]
|
||||
pub message_type: Option<u8>,
|
||||
pub body: Option<String>,
|
||||
pub message_marks: Option<String>,
|
||||
pub create_time: Option<i64>,
|
||||
pub update_time: Option<i64>,
|
||||
#[serde(skip)]
|
||||
#[sea_orm(primary_key)]
|
||||
pub login_uid: String,
|
||||
/// 消息发送状态: pending, success, fail
|
||||
pub send_status: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,23 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "im_room")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
pub room_id: String,
|
||||
pub group_id: Option<String>,
|
||||
pub room_name: String,
|
||||
pub avatar: Option<String>,
|
||||
#[serde(skip)]
|
||||
#[sea_orm(primary_key)]
|
||||
pub login_uid: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,31 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "im_room_member")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
pub room_id: Option<String>,
|
||||
pub uid: Option<String>,
|
||||
pub account: Option<String>,
|
||||
pub my_name: Option<String>,
|
||||
pub active_status: Option<u8>,
|
||||
pub group_role: Option<i64>,
|
||||
pub loc_place: Option<String>,
|
||||
pub last_opt_time: i64,
|
||||
pub create_time: Option<i64>,
|
||||
pub name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub user_state_id: Option<String>,
|
||||
#[serde(skip)]
|
||||
#[sea_orm(primary_key)]
|
||||
pub login_uid: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,36 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "im_user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: String,
|
||||
pub user_id: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub account: Option<String>,
|
||||
pub sex: Option<i32>,
|
||||
pub open_id: Option<String>,
|
||||
pub active_status: Option<i32>,
|
||||
pub user_state_id: Option<i64>,
|
||||
pub last_opt_time: Option<i64>,
|
||||
pub ip_info: Option<Json>,
|
||||
pub item_id: Option<i64>,
|
||||
pub state: Option<i32>,
|
||||
pub create_time: Option<i64>,
|
||||
pub update_time: Option<i64>,
|
||||
pub password: Option<String>,
|
||||
pub avatar_update_time: Option<i64>,
|
||||
pub num: Option<i32>,
|
||||
pub context: Option<bool>,
|
||||
pub user_type: Option<i32>,
|
||||
pub is_init: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod im_config;
|
||||
pub mod im_contact;
|
||||
pub mod im_message;
|
||||
pub mod im_room;
|
||||
pub mod im_room_member;
|
||||
pub mod im_user;
|
||||
pub mod prelude;
|
||||
@@ -0,0 +1 @@
|
||||
pub use super::im_user::Entity as ImUserEntity;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6422,6 +6422,24 @@
|
||||
"const": "http:deny-fetch-send",
|
||||
"markdownDescription": "Denies the fetch_send command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Allows the log command\n#### This default permission set includes:\n\n- `allow-log`",
|
||||
"type": "string",
|
||||
"const": "log:default",
|
||||
"markdownDescription": "Allows the log command\n#### This default permission set includes:\n\n- `allow-log`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "log:allow-log",
|
||||
"markdownDescription": "Enables the log command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "log:deny-log",
|
||||
"markdownDescription": "Denies the log command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start-recording`\n- `allow-stop-recording`",
|
||||
"type": "string",
|
||||
|
||||
@@ -6422,6 +6422,24 @@
|
||||
"const": "http:deny-fetch-send",
|
||||
"markdownDescription": "Denies the fetch_send command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Allows the log command\n#### This default permission set includes:\n\n- `allow-log`",
|
||||
"type": "string",
|
||||
"const": "log:default",
|
||||
"markdownDescription": "Allows the log command\n#### This default permission set includes:\n\n- `allow-log`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "log:allow-log",
|
||||
"markdownDescription": "Enables the log command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the log command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "log:deny-log",
|
||||
"markdownDescription": "Denies the log command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-start-recording`\n- `allow-stop-recording`",
|
||||
"type": "string",
|
||||
|
||||
Generated
+1887
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "migration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "migration"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||
entity.workspace = true
|
||||
|
||||
[dependencies.sea-orm-migration]
|
||||
version = "1.1.0"
|
||||
features = [
|
||||
"sqlx-sqlite",
|
||||
"runtime-tokio-rustls"
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
# Running Migrator CLI
|
||||
|
||||
- Generate a new migration file
|
||||
```sh
|
||||
cargo run -- generate MIGRATION_NAME
|
||||
```
|
||||
- Apply all pending migrations
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
```sh
|
||||
cargo run -- up
|
||||
```
|
||||
- Apply first 10 pending migrations
|
||||
```sh
|
||||
cargo run -- up -n 10
|
||||
```
|
||||
- Rollback last applied migrations
|
||||
```sh
|
||||
cargo run -- down
|
||||
```
|
||||
- Rollback last 10 applied migrations
|
||||
```sh
|
||||
cargo run -- down -n 10
|
||||
```
|
||||
- Drop all tables from the database, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- fresh
|
||||
```
|
||||
- Rollback all applied migrations, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- refresh
|
||||
```
|
||||
- Rollback all applied migrations
|
||||
```sh
|
||||
cargo run -- reset
|
||||
```
|
||||
- Check the status of all migrations
|
||||
```sh
|
||||
cargo run -- status
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20220101_000001_create_table;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![Box::new(m20220101_000001_create_table::Migration)]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 创建 im_user 表
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ImUser::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(ImUser::Id).string().not_null().primary_key())
|
||||
.col(ColumnDef::new(ImUser::UserId).big_integer())
|
||||
.col(ColumnDef::new(ImUser::Name).string())
|
||||
.col(ColumnDef::new(ImUser::Avatar).string())
|
||||
.col(ColumnDef::new(ImUser::Email).string())
|
||||
.col(ColumnDef::new(ImUser::Account).string())
|
||||
.col(ColumnDef::new(ImUser::Sex).integer())
|
||||
.col(ColumnDef::new(ImUser::OpenId).string())
|
||||
.col(ColumnDef::new(ImUser::ActiveStatus).integer())
|
||||
.col(ColumnDef::new(ImUser::UserStateId).big_integer())
|
||||
.col(ColumnDef::new(ImUser::LastOptTime).big_integer())
|
||||
.col(ColumnDef::new(ImUser::IpInfo).json())
|
||||
.col(ColumnDef::new(ImUser::ItemId).big_integer())
|
||||
.col(ColumnDef::new(ImUser::State).integer())
|
||||
.col(ColumnDef::new(ImUser::CreateTime).big_integer())
|
||||
.col(ColumnDef::new(ImUser::UpdateTime).big_integer())
|
||||
.col(ColumnDef::new(ImUser::Password).string())
|
||||
.col(ColumnDef::new(ImUser::AvatarUpdateTime).big_integer())
|
||||
.col(ColumnDef::new(ImUser::Num).integer())
|
||||
.col(ColumnDef::new(ImUser::Context).boolean())
|
||||
.col(ColumnDef::new(ImUser::UserType).integer())
|
||||
.col(
|
||||
ColumnDef::new(ImUser::IsInit)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 创建 im_contact 表
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ImContact::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(ImContact::Id).string().not_null())
|
||||
.col(ColumnDef::new(ImContact::DetailId).string().not_null())
|
||||
.col(ColumnDef::new(ImContact::RoomId).string().not_null())
|
||||
.col(ColumnDef::new(ImContact::ContactType).tiny_unsigned())
|
||||
.col(ColumnDef::new(ImContact::HotFlag).tiny_unsigned())
|
||||
.col(ColumnDef::new(ImContact::Top).boolean())
|
||||
.col(ColumnDef::new(ImContact::Account).string())
|
||||
.col(ColumnDef::new(ImContact::Operate).tiny_unsigned())
|
||||
.col(ColumnDef::new(ImContact::Remark).string())
|
||||
.col(ColumnDef::new(ImContact::MyName).string())
|
||||
.col(ColumnDef::new(ImContact::MuteNotification).tiny_unsigned())
|
||||
.col(ColumnDef::new(ImContact::Hide).boolean())
|
||||
.col(ColumnDef::new(ImContact::ActiveTime).big_integer())
|
||||
.col(ColumnDef::new(ImContact::Shield).boolean())
|
||||
.col(ColumnDef::new(ImContact::Avatar).string())
|
||||
.col(ColumnDef::new(ImContact::ContactName).string())
|
||||
.col(ColumnDef::new(ImContact::Text).string())
|
||||
.col(ColumnDef::new(ImContact::UnreadCount).tiny_unsigned())
|
||||
.col(ColumnDef::new(ImContact::CreateTime).big_integer())
|
||||
.col(ColumnDef::new(ImContact::UpdateTime).big_integer())
|
||||
.col(ColumnDef::new(ImContact::LoginUid).string().not_null())
|
||||
.primary_key(Index::create().col(ImContact::Id).col(ImContact::LoginUid))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 创建 im_room 表
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ImRoom::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(ImRoom::Id).string().not_null())
|
||||
.col(ColumnDef::new(ImRoom::RoomId).string().not_null())
|
||||
.col(ColumnDef::new(ImRoom::GroupId).string())
|
||||
.col(ColumnDef::new(ImRoom::RoomName).string().not_null())
|
||||
.col(ColumnDef::new(ImRoom::Avatar).string())
|
||||
.col(ColumnDef::new(ImRoom::LoginUid).string().not_null())
|
||||
.primary_key(Index::create().col(ImRoom::Id).col(ImRoom::LoginUid))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 创建 im_room_member 表
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ImRoomMember::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(ImRoomMember::Id).string().not_null())
|
||||
.col(ColumnDef::new(ImRoomMember::RoomId).string())
|
||||
.col(ColumnDef::new(ImRoomMember::Uid).string())
|
||||
.col(ColumnDef::new(ImRoomMember::Account).string())
|
||||
.col(ColumnDef::new(ImRoomMember::MyName).string())
|
||||
.col(ColumnDef::new(ImRoomMember::ActiveStatus).tiny_unsigned())
|
||||
.col(ColumnDef::new(ImRoomMember::GroupRole).big_integer())
|
||||
.col(ColumnDef::new(ImRoomMember::LocPlace).string())
|
||||
.col(
|
||||
ColumnDef::new(ImRoomMember::LastOptTime)
|
||||
.big_integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(ImRoomMember::CreateTime).big_integer())
|
||||
.col(ColumnDef::new(ImRoomMember::Name).string().not_null())
|
||||
.col(ColumnDef::new(ImRoomMember::Avatar).string())
|
||||
.col(ColumnDef::new(ImRoomMember::UserStateId).string())
|
||||
.col(ColumnDef::new(ImRoomMember::LoginUid).string().not_null())
|
||||
.primary_key(
|
||||
Index::create()
|
||||
.col(ImRoomMember::Id)
|
||||
.col(ImRoomMember::LoginUid),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 创建 im_message 表
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ImMessage::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(ImMessage::Id).string().not_null())
|
||||
.col(ColumnDef::new(ImMessage::Uid).string().not_null())
|
||||
.col(ColumnDef::new(ImMessage::Nickname).string())
|
||||
.col(ColumnDef::new(ImMessage::RoomId).string().not_null())
|
||||
.col(ColumnDef::new(ImMessage::SendTime).big_integer())
|
||||
.col(ColumnDef::new(ImMessage::MessageType).tiny_unsigned())
|
||||
.col(ColumnDef::new(ImMessage::Body).string())
|
||||
.col(ColumnDef::new(ImMessage::MessageMarks).string())
|
||||
.col(ColumnDef::new(ImMessage::CreateTime).big_integer())
|
||||
.col(ColumnDef::new(ImMessage::UpdateTime).big_integer())
|
||||
.col(ColumnDef::new(ImMessage::LoginUid).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(ImMessage::SendStatus)
|
||||
.string()
|
||||
.not_null()
|
||||
.default("pending"),
|
||||
)
|
||||
.primary_key(Index::create().col(ImMessage::Id).col(ImMessage::LoginUid))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 创建 im_config 表
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ImConfig::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(ImConfig::Id).big_integer().not_null())
|
||||
.col(ColumnDef::new(ImConfig::ConfigKey).string().not_null())
|
||||
.col(ColumnDef::new(ImConfig::ConfigValue).string())
|
||||
.col(ColumnDef::new(ImConfig::LoginUid).string().not_null())
|
||||
.primary_key(Index::create().col(ImConfig::Id).col(ImConfig::LoginUid))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(ImConfig::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(ImMessage::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(ImRoomMember::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(ImRoom::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(ImContact::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(ImUser::Table).to_owned())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// 定义表和列的枚举
|
||||
#[derive(DeriveIden)]
|
||||
enum ImUser {
|
||||
Table,
|
||||
Id,
|
||||
UserId,
|
||||
Name,
|
||||
Avatar,
|
||||
Email,
|
||||
Account,
|
||||
Sex,
|
||||
OpenId,
|
||||
ActiveStatus,
|
||||
UserStateId,
|
||||
LastOptTime,
|
||||
IpInfo,
|
||||
ItemId,
|
||||
State,
|
||||
CreateTime,
|
||||
UpdateTime,
|
||||
Password,
|
||||
AvatarUpdateTime,
|
||||
Num,
|
||||
Context,
|
||||
UserType,
|
||||
IsInit,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ImContact {
|
||||
Table,
|
||||
Id,
|
||||
DetailId,
|
||||
RoomId,
|
||||
ContactType,
|
||||
HotFlag,
|
||||
Top,
|
||||
Account,
|
||||
Operate,
|
||||
Remark,
|
||||
MyName,
|
||||
MuteNotification,
|
||||
Hide,
|
||||
ActiveTime,
|
||||
Shield,
|
||||
Avatar,
|
||||
ContactName,
|
||||
Text,
|
||||
UnreadCount,
|
||||
CreateTime,
|
||||
UpdateTime,
|
||||
LoginUid,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ImRoom {
|
||||
Table,
|
||||
Id,
|
||||
RoomId,
|
||||
GroupId,
|
||||
RoomName,
|
||||
Avatar,
|
||||
LoginUid,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ImRoomMember {
|
||||
Table,
|
||||
Id,
|
||||
RoomId,
|
||||
Uid,
|
||||
Account,
|
||||
MyName,
|
||||
ActiveStatus,
|
||||
GroupRole,
|
||||
LocPlace,
|
||||
LastOptTime,
|
||||
CreateTime,
|
||||
Name,
|
||||
Avatar,
|
||||
UserStateId,
|
||||
LoginUid,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ImMessage {
|
||||
Table,
|
||||
Id,
|
||||
Uid,
|
||||
Nickname,
|
||||
RoomId,
|
||||
SendTime,
|
||||
MessageType,
|
||||
Body,
|
||||
MessageMarks,
|
||||
CreateTime,
|
||||
UpdateTime,
|
||||
LoginUid,
|
||||
SendStatus,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ImConfig {
|
||||
Table,
|
||||
Id,
|
||||
ConfigKey,
|
||||
ConfigValue,
|
||||
LoginUid,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
cli::run_cli(migration::Migrator).await;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
use crate::AppData;
|
||||
use crate::error::CommonError;
|
||||
use crate::im_reqest_client::ImRequestClient;
|
||||
use crate::repository::im_contact_repository::{list_contact, save_contact_batch};
|
||||
use anyhow::Context;
|
||||
use entity::im_contact;
|
||||
use log::error;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_contacts_command(
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<Vec<im_contact::Model>, String> {
|
||||
let result: Result<Vec<im_contact::Model>, CommonError> = async {
|
||||
// 获取当前登录用户的 uid
|
||||
let login_uid = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
// 检查缓存中是否存在联系人列表数据
|
||||
let cache_key = format!("contacts_list_{}", login_uid);
|
||||
let is_cached = state.cache.get(&cache_key).await.is_some();
|
||||
|
||||
if !is_cached {
|
||||
// 第一次查询:先调用后端接口,再更新本地数据库
|
||||
let data = fetch_and_update_contacts(
|
||||
state.db_conn.clone(),
|
||||
state.request_client.clone(),
|
||||
login_uid.clone(),
|
||||
)
|
||||
.await?;
|
||||
// 设置缓存标记
|
||||
state.cache.insert(cache_key, "cached".to_string()).await;
|
||||
return Ok(data);
|
||||
} else {
|
||||
// 有缓存:从本地数据库获取数据
|
||||
let local_contacts = list_contact(state.db_conn.deref(), &login_uid)
|
||||
.await
|
||||
.with_context(|| format!("[{}:{}] 本地数据库查询联系人失败", file!(), line!()))?;
|
||||
|
||||
// 异步调用后端接口更新本地数据库
|
||||
let db_conn = state.db_conn.clone();
|
||||
let request_client = state.request_client.clone();
|
||||
let login_uid_clone = login_uid.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
fetch_and_update_contacts(db_conn, request_client, login_uid_clone).await
|
||||
{
|
||||
error!("异步更新联系人数据失败: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(local_contacts)
|
||||
}
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(contacts) => Ok(contacts),
|
||||
Err(e) => {
|
||||
error!("获取联系人列表失败: {:?}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取并更新联系人数据
|
||||
async fn fetch_and_update_contacts(
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
request_client: Arc<Mutex<ImRequestClient>>,
|
||||
login_uid: String,
|
||||
) -> Result<Vec<im_contact::Model>, CommonError> {
|
||||
// 从后端API获取联系人数据
|
||||
let resp = request_client
|
||||
.lock()
|
||||
.await
|
||||
.get("/chat/contact/list")
|
||||
.send_json::<Vec<im_contact::Model>>()
|
||||
.await?;
|
||||
|
||||
if let Some(data) = resp.data {
|
||||
// 保存到本地数据库
|
||||
save_contact_batch(db_conn.deref(), data.clone(), &login_uid)
|
||||
.await
|
||||
.with_context(|| format!("[{}:{}] 保存联系人数据到本地数据库失败", file!(), line!()))?;
|
||||
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(CommonError::UnexpectedError(anyhow::anyhow!(
|
||||
"获取联系人数据失败"
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
use crate::AppData;
|
||||
use crate::error::CommonError;
|
||||
use crate::im_reqest_client::ImRequestClient;
|
||||
use crate::pojo::common::{CursorPageParam, CursorPageResp};
|
||||
use crate::repository::{im_message_repository, im_user_repository};
|
||||
use crate::vo::vo::ChatMessageReq;
|
||||
use anyhow::Context;
|
||||
use entity::im_user::Entity as ImUserEntity;
|
||||
use entity::{im_message, im_user};
|
||||
use log::{debug, error, info};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use sea_orm::{DatabaseConnection, TransactionTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageResp {
|
||||
pub create_id: Option<String>,
|
||||
pub create_time: Option<i64>,
|
||||
pub update_id: Option<String>,
|
||||
pub update_time: Option<i64>,
|
||||
pub from_user: FromUser,
|
||||
pub message: Message,
|
||||
pub old_msg_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FromUser {
|
||||
pub uid: String,
|
||||
pub nickname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Message {
|
||||
pub id: Option<String>,
|
||||
pub room_id: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub message_type: Option<u8>,
|
||||
pub body: Option<serde_json::Value>,
|
||||
pub message_marks: Option<HashMap<String, MessageMark>>,
|
||||
pub send_time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UrlInfo {
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MergeMessage {
|
||||
pub content: Option<String>,
|
||||
pub created_time: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReplyMsg {
|
||||
pub id: Option<String>,
|
||||
pub uid: Option<String>,
|
||||
pub username: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub msg_type: Option<u8>,
|
||||
pub body: Option<Box<serde_json::Value>>,
|
||||
pub can_callback: u8,
|
||||
pub gap_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageMark {
|
||||
pub count: u32,
|
||||
pub user_marked: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CursorPageMessageParam {
|
||||
room_id: String,
|
||||
#[serde(flatten)]
|
||||
cursor_page_param: CursorPageParam,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn page_msg(
|
||||
param: CursorPageMessageParam,
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<CursorPageResp<Vec<MessageResp>>, String> {
|
||||
// 获取当前登录用户的 uid
|
||||
let login_uid = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
// 从数据库查询消息
|
||||
let db_result = im_message_repository::cursor_page_messages(
|
||||
state.db_conn.deref(),
|
||||
param.room_id,
|
||||
param.cursor_page_param,
|
||||
&login_uid,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 转换数据库模型为响应模型
|
||||
let message_resps: Vec<MessageResp> = db_result
|
||||
.list
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|msg| convert_message_to_resp(msg))
|
||||
.rev()
|
||||
.collect();
|
||||
|
||||
Ok(CursorPageResp {
|
||||
cursor: db_result.cursor,
|
||||
is_last: db_result.is_last,
|
||||
list: Some(message_resps),
|
||||
total: db_result.total,
|
||||
})
|
||||
}
|
||||
|
||||
/// 将数据库消息模型转换为响应模型
|
||||
fn convert_message_to_resp(msg: im_message::Model) -> MessageResp {
|
||||
// 解析消息体
|
||||
let body = msg.body.as_ref().and_then(|b| serde_json::from_str(b).ok());
|
||||
|
||||
// 解析消息标记
|
||||
let message_marks = msg
|
||||
.message_marks
|
||||
.as_ref()
|
||||
.and_then(|marks| serde_json::from_str::<HashMap<String, MessageMark>>(marks).ok());
|
||||
|
||||
MessageResp {
|
||||
create_id: Some(msg.id.clone()),
|
||||
create_time: msg.send_time,
|
||||
update_id: None,
|
||||
update_time: None,
|
||||
from_user: FromUser {
|
||||
uid: msg.uid,
|
||||
nickname: msg.nickname,
|
||||
},
|
||||
message: Message {
|
||||
id: Some(msg.id),
|
||||
room_id: Some(msg.room_id),
|
||||
message_type: msg.message_type,
|
||||
body,
|
||||
message_marks,
|
||||
send_time: msg.send_time,
|
||||
},
|
||||
old_msg_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查用户初始化状态并获取消息
|
||||
pub async fn check_user_init_and_fetch_messages(
|
||||
client: &ImRequestClient,
|
||||
db_conn: &DatabaseConnection,
|
||||
uid: &str,
|
||||
) -> Result<(), CommonError> {
|
||||
debug!("检查用户初始化状态并获取消息, uid: {}", uid);
|
||||
// 检查用户的 is_init 状态
|
||||
if let Ok(user) = ImUserEntity::find()
|
||||
.filter(im_user::Column::Id.eq(uid))
|
||||
.one(db_conn)
|
||||
.await
|
||||
{
|
||||
if let Some(user_model) = user {
|
||||
// 如果 is_init 为 true,调用后端接口获取所有消息
|
||||
if user_model.is_init {
|
||||
info!("用户 {} 需要初始化,开始获取所有消息", uid);
|
||||
// 传递用户的 last_opt_time 参数
|
||||
if let Err(e) = fetch_all_messages(client, db_conn, uid, None).await {
|
||||
error!("获取所有消息失败: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}else {
|
||||
info!("用户 {} 离线消息更新, last_opt_time: {:?}", uid, user_model.last_opt_time);
|
||||
if let Err(e) = fetch_all_messages(client, db_conn, uid, user_model.last_opt_time).await {
|
||||
error!("离线消息更新失败: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从后端获取所有消息并保存到数据库
|
||||
pub async fn fetch_all_messages(
|
||||
client: &ImRequestClient,
|
||||
db_conn: &DatabaseConnection,
|
||||
uid: &str,
|
||||
last_opt_time: Option<i64>,
|
||||
) -> Result<(), CommonError> {
|
||||
// 调用后端接口 /chat/msg/list 获取所有消息,传递 last_opt_time 参数
|
||||
let mut request = client.get("/chat/msg/list");
|
||||
|
||||
// 如果有 last_opt_time 参数,添加到查询参数中
|
||||
if let Some(time) = last_opt_time {
|
||||
request = request.query(&[("lastOptTime", time.to_string())]);
|
||||
}
|
||||
|
||||
let messages = request
|
||||
.send_json::<Vec<MessageResp>>()
|
||||
.await?;
|
||||
|
||||
if let Some(messages) = messages.data {
|
||||
// 开启事务
|
||||
let tx = db_conn.begin().await?;
|
||||
|
||||
// 转换 MessageResp 为 im_message::Model
|
||||
let db_messages: Vec<im_message::Model> = messages
|
||||
.into_iter()
|
||||
.map(|msg_resp| convert_resp_to_model_for_fetch(msg_resp, uid.to_string()))
|
||||
.collect();
|
||||
// 保存到本地数据库
|
||||
match im_message_repository::save_all(&tx, db_messages).await {
|
||||
Ok(_) => {
|
||||
info!("消息保存到数据库成功");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("保存消息到数据库失败,详细错误: {:?}", e);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
|
||||
// 消息保存完成后,将用户的 is_init 状态设置为 false
|
||||
im_user_repository::update_user_init_status(&tx, uid, false)
|
||||
.await
|
||||
.with_context(|| "更新用户 is_init 状态失败")?;
|
||||
|
||||
// 提交事务
|
||||
tx.commit().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将 MessageResp 转换为数据库模型(用于 fetch_all_messages)
|
||||
fn convert_resp_to_model_for_fetch(msg_resp: MessageResp, uid: String) -> im_message::Model {
|
||||
use serde_json;
|
||||
|
||||
// 序列化消息体为 JSON 字符串
|
||||
let body_json = msg_resp
|
||||
.message
|
||||
.body
|
||||
.as_ref()
|
||||
.and_then(|body| serde_json::to_string(body).ok());
|
||||
|
||||
// 序列化消息标记为 JSON 字符串
|
||||
let marks_json = msg_resp
|
||||
.message
|
||||
.message_marks
|
||||
.as_ref()
|
||||
.and_then(|marks| serde_json::to_string(marks).ok());
|
||||
|
||||
im_message::Model {
|
||||
id: msg_resp.message.id.unwrap_or_default(),
|
||||
uid: msg_resp.from_user.uid,
|
||||
nickname: msg_resp.from_user.nickname,
|
||||
room_id: msg_resp.message.room_id.unwrap_or_default(),
|
||||
message_type: msg_resp.message.message_type,
|
||||
body: body_json,
|
||||
message_marks: marks_json,
|
||||
send_time: msg_resp.message.send_time,
|
||||
create_time: msg_resp.create_time,
|
||||
update_time: msg_resp.update_time,
|
||||
login_uid: uid.to_string(), // 这里暂时设为空字符串,实际使用时会在 save_all 中设置
|
||||
send_status: "success".to_string(), // 从后端获取的消息默认为成功状态
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_msg(
|
||||
data: ChatMessageReq,
|
||||
state: State<'_, AppData>,
|
||||
app: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
use std::ops::Deref;
|
||||
|
||||
// 获取当前登录用户信息
|
||||
let (login_uid, nickname) = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
(user_info.uid.clone(), None) // UserInfo只有uid和token字段,nickname暂时设为None
|
||||
};
|
||||
|
||||
// 生成消息ID
|
||||
let current_time = chrono::Utc::now().timestamp_millis();
|
||||
|
||||
// 先克隆data以避免所有权问题
|
||||
let send_data = data.clone();
|
||||
|
||||
// 序列化消息体
|
||||
let body_json = data
|
||||
.body
|
||||
.as_ref()
|
||||
.and_then(|body| serde_json::to_string(body).ok());
|
||||
|
||||
// 创建消息模型
|
||||
let message = im_message::Model {
|
||||
id: data.id.clone(),
|
||||
uid: login_uid.clone(),
|
||||
nickname,
|
||||
room_id: data.room_id.unwrap_or_default(),
|
||||
message_type: data.msg_type,
|
||||
body: body_json,
|
||||
message_marks: None,
|
||||
send_time: Some(current_time),
|
||||
create_time: Some(current_time),
|
||||
update_time: Some(current_time),
|
||||
login_uid: login_uid.clone(),
|
||||
send_status: "pending".to_string(), // 初始状态为pending
|
||||
};
|
||||
|
||||
let tx = state
|
||||
.db_conn
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| CommonError::DatabaseError(e))?;
|
||||
// 先保存到本地数据库
|
||||
if let Err(e) = im_message_repository::save_message(&tx, message.clone()).await {
|
||||
error!("保存消息到数据库失败: {}", e);
|
||||
return Err(e.to_string());
|
||||
}
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e| CommonError::DatabaseError(e))?;
|
||||
|
||||
info!("消息已保存到本地数据库,ID: {}", message.id.clone());
|
||||
|
||||
// 异步发送到后端接口
|
||||
let db_conn = state.db_conn.clone();
|
||||
let request_client = state.request_client.clone();
|
||||
let msg_id = message.id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// 发送到后端接口
|
||||
let result = {
|
||||
let client = request_client.lock().await;
|
||||
client
|
||||
.post("/chat/msg")
|
||||
.json(&send_data)
|
||||
.send_json::<MessageResp>()
|
||||
.await
|
||||
};
|
||||
|
||||
let mut id = None;
|
||||
|
||||
// 根据发送结果更新消息状态
|
||||
let status = match result {
|
||||
Ok(resp) => {
|
||||
info!("消息发送成功,ID: {}", msg_id);
|
||||
let mut result = resp.data.clone().unwrap();
|
||||
result.old_msg_id = Some(msg_id.clone());
|
||||
id = result.message.id.clone();
|
||||
|
||||
let _ = app.emit::<MessageResp>("send_msg_success", result);
|
||||
"success"
|
||||
}
|
||||
Err(e) => {
|
||||
error!("消息发送失败,ID: {}, 错误: {}", msg_id, e);
|
||||
let _ = app.emit::<String>("send_msg_error", msg_id.clone());
|
||||
"fail"
|
||||
}
|
||||
};
|
||||
|
||||
// 更新消息状态
|
||||
if let Err(e) = im_message_repository::update_message_status(
|
||||
db_conn.deref(),
|
||||
&msg_id,
|
||||
status,
|
||||
id,
|
||||
login_uid.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("更新消息状态失败: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_msg(data: MessageResp, state: State<'_, AppData>) -> Result<(), String> {
|
||||
info!("收到消息保存到本地, data: {:?}", data);
|
||||
// 创建 im_message::Model
|
||||
let message = convert_resp_to_model_for_fetch(data, state.user_info.lock().await.uid.clone());
|
||||
|
||||
async {
|
||||
let tx = state.db_conn.clone().begin().await?;
|
||||
// 保存到数据库
|
||||
im_message_repository::save_message(&tx, message).await?;
|
||||
tx.commit().await?;
|
||||
Ok::<(), CommonError>(())
|
||||
}
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod contact_command;
|
||||
pub mod message_command;
|
||||
pub mod room_member_command;
|
||||
pub mod user_command;
|
||||
@@ -0,0 +1,333 @@
|
||||
use crate::AppData;
|
||||
use crate::error::CommonError;
|
||||
use crate::pojo::common::{CursorPageParam, CursorPageResp, Page, PageParam};
|
||||
use crate::repository::im_room_member_repository::{
|
||||
get_room_members_by_room_id, get_room_page, save_room_batch, save_room_member_batch,
|
||||
update_my_room_info as update_my_room_info_db,
|
||||
};
|
||||
use crate::vo::vo::MyRoomInfoReq;
|
||||
use anyhow::Context;
|
||||
use entity::{im_room, im_room_member};
|
||||
use log::{error, info};
|
||||
|
||||
use crate::im_reqest_client::ImRequestClient;
|
||||
use crate::repository::im_room_member_repository;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_my_room_info(
|
||||
my_room_info: MyRoomInfoReq,
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<(), String> {
|
||||
let result: Result<(), CommonError> = async {
|
||||
// 获取当前用户信息
|
||||
let user_info = state.user_info.lock().await;
|
||||
let uid = user_info.uid.clone();
|
||||
drop(user_info);
|
||||
|
||||
// 调用后端接口更新房间信息
|
||||
let _resp = state
|
||||
.request_client
|
||||
.lock()
|
||||
.await
|
||||
.post("/room/updateMyRoomInfo")
|
||||
.json(&my_room_info)
|
||||
.send_json::<bool>()
|
||||
.await
|
||||
.with_context(|| format!("[{}:{}] 调用后端接口更新房间信息失败", file!(), line!()))?;
|
||||
|
||||
// 更新本地数据库
|
||||
update_my_room_info_db(
|
||||
state.db_conn.deref(),
|
||||
&my_room_info.my_name,
|
||||
&my_room_info.id,
|
||||
&uid,
|
||||
&uid,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("[{}:{}] 更新本地数据库失败", file!(), line!()))?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(members) => Ok(members),
|
||||
Err(e) => {
|
||||
error!("更新房间信息失败: {:?}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取room_id的房间的所有成员列表
|
||||
#[tauri::command]
|
||||
pub async fn get_room_members(
|
||||
room_id: String,
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<Vec<im_room_member::Model>, String> {
|
||||
info!("调用获取room_id的房间的所有成员列表");
|
||||
let result: Result<Vec<im_room_member::Model>, CommonError> = async {
|
||||
// 检查缓存中是否存在该room_id
|
||||
let cache_key = format!("room_members_{}", room_id);
|
||||
let is_cached = state.cache.get(&cache_key).await.is_some();
|
||||
|
||||
if !is_cached {
|
||||
// 获取当前登录用户的 uid
|
||||
let login_uid = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
let mut data = fetch_and_update_room_members(
|
||||
room_id.clone(),
|
||||
state.db_conn.clone(),
|
||||
state.request_client.clone(),
|
||||
login_uid.clone(),
|
||||
)
|
||||
.await?;
|
||||
// 设置缓存标记
|
||||
state.cache.insert(cache_key, "cached".to_string()).await;
|
||||
// 对从后端获取的数据进行排序
|
||||
sort_room_members(&mut data);
|
||||
return Ok(data);
|
||||
} else {
|
||||
// 获取当前登录用户的 uid
|
||||
let login_uid = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
// 有缓存:从本地数据库获取数据
|
||||
let mut local_members =
|
||||
get_room_members_by_room_id(&room_id, state.db_conn.deref(), &login_uid)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("[{}:{}] 本地数据库查询房间成员失败", file!(), line!())
|
||||
})?;
|
||||
|
||||
// 对查询结果进行排序
|
||||
sort_room_members(&mut local_members);
|
||||
|
||||
// 异步调用后端接口更新本地数据库
|
||||
let db_conn = state.db_conn.clone();
|
||||
let request_client = state.request_client.clone();
|
||||
let room_id_clone = room_id.clone();
|
||||
let login_uid_clone = login_uid.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = fetch_and_update_room_members(
|
||||
room_id_clone,
|
||||
db_conn,
|
||||
request_client,
|
||||
login_uid_clone,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("异步更新房间成员数据失败: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(local_members)
|
||||
}
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(members) => Ok(members),
|
||||
Err(e) => {
|
||||
error!("获取房间全部成员数据失败: {:?}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CursorPageRoomMemberParam {
|
||||
room_id: String,
|
||||
#[serde(flatten)]
|
||||
cursor_page_param: CursorPageParam,
|
||||
}
|
||||
|
||||
// 游标分页查询数据
|
||||
#[tauri::command]
|
||||
pub async fn cursor_page_room_members(
|
||||
param: CursorPageRoomMemberParam,
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<CursorPageResp<Vec<im_room_member::Model>>, String> {
|
||||
// 获取当前登录用户的 uid
|
||||
let login_uid = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
let data = im_room_member_repository::cursor_page_room_members(
|
||||
state.db_conn.deref(),
|
||||
param.room_id,
|
||||
param.cursor_page_param,
|
||||
&login_uid,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 从本地数据库分页查询房间数据,如果为空则从后端获取
|
||||
#[tauri::command]
|
||||
pub async fn page_room(
|
||||
page_param: PageParam,
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<Page<im_room::Model>, String> {
|
||||
let result: Result<Page<im_room::Model>, CommonError> = async {
|
||||
// 检查缓存中是否存在房间列表数据
|
||||
let cache_key = format!("room_list_page_{}_{}", page_param.current, page_param.size);
|
||||
let is_cached = state.cache.get(&cache_key).await.is_some();
|
||||
|
||||
if !is_cached {
|
||||
// 获取当前登录用户的 uid
|
||||
let login_uid = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
// 第一次查询:先调用后端接口,再更新本地数据库
|
||||
let data = fetch_and_update_rooms(
|
||||
page_param.clone(),
|
||||
state.db_conn.clone(),
|
||||
state.request_client.clone(),
|
||||
login_uid,
|
||||
)
|
||||
.await?;
|
||||
// 设置缓存标记
|
||||
state.cache.insert(cache_key, "cached".to_string()).await;
|
||||
return Ok(data);
|
||||
} else {
|
||||
// 获取当前登录用户的 uid
|
||||
let login_uid = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
user_info.uid.clone()
|
||||
};
|
||||
|
||||
// 有缓存:从本地数据库获取数据
|
||||
let local_result = get_room_page(page_param.clone(), state.db_conn.deref(), &login_uid)
|
||||
.await
|
||||
.with_context(|| format!("[{}:{}] 本地数据库查询失败", file!(), line!()))?;
|
||||
|
||||
// 异步调用后端接口更新本地数据库
|
||||
let db_conn = state.db_conn.clone();
|
||||
let request_client = state.request_client.clone();
|
||||
let page_param_clone = page_param.clone();
|
||||
let login_uid_clone = login_uid.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = fetch_and_update_rooms(
|
||||
page_param_clone,
|
||||
db_conn,
|
||||
request_client,
|
||||
login_uid_clone,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("异步更新房间数据失败: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(local_result)
|
||||
}
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(page_data) => Ok(page_data),
|
||||
Err(e) => {
|
||||
error!("分页获取房间数据失败: {:?}", e);
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 对房间成员列表进行排序:在线用户优先(active_status=1),相同状态下按last_opt_time降序
|
||||
fn sort_room_members(members: &mut Vec<im_room_member::Model>) {
|
||||
members.sort_by(|a, b| {
|
||||
let a_status = a.active_status.unwrap_or(0);
|
||||
let b_status = b.active_status.unwrap_or(0);
|
||||
|
||||
// 先按active_status升序排序(在线用户优先)
|
||||
match a_status.cmp(&b_status) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
// active_status相同时,按last_opt_time降序排序
|
||||
b.last_opt_time.cmp(&a.last_opt_time)
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 异步更新房间成员数据
|
||||
async fn fetch_and_update_room_members(
|
||||
room_id: String,
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
request_client: Arc<Mutex<ImRequestClient>>,
|
||||
login_uid: String,
|
||||
) -> Result<Vec<im_room_member::Model>, CommonError> {
|
||||
// 从后端API获取最新数据
|
||||
let resp = request_client
|
||||
.lock()
|
||||
.await
|
||||
.get("/room/group/listMember")
|
||||
.query(&[("roomId", &room_id)])
|
||||
.send_json::<Vec<im_room_member::Model>>()
|
||||
.await?;
|
||||
|
||||
// 更新本地数据库
|
||||
if let Some(data) = resp.data {
|
||||
if !data.is_empty() {
|
||||
let room_id_i64 = room_id.parse::<i64>().unwrap_or(0);
|
||||
save_room_member_batch(db_conn.deref(), data.clone(), room_id_i64, &login_uid)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"[{}:{}] 异步更新房间成员数据到本地数据库失败",
|
||||
file!(),
|
||||
line!()
|
||||
)
|
||||
})?;
|
||||
return Ok(data.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// 获取并更新房间数据
|
||||
async fn fetch_and_update_rooms(
|
||||
page_param: PageParam,
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
request_client: Arc<Mutex<ImRequestClient>>,
|
||||
login_uid: String,
|
||||
) -> Result<Page<im_room::Model>, CommonError> {
|
||||
// 从后端API获取数据
|
||||
let resp = request_client
|
||||
.lock()
|
||||
.await
|
||||
.get("/room/group/list")
|
||||
.query(&page_param)
|
||||
.send_json::<Page<im_room::Model>>()
|
||||
.await?;
|
||||
|
||||
if let Some(data) = resp.data {
|
||||
// 保存到本地数据库
|
||||
save_room_batch(db_conn.deref(), data.records.clone(), &login_uid)
|
||||
.await
|
||||
.with_context(|| format!("[{}:{}] 保存房间数据到本地数据库失败", file!(), line!()))?;
|
||||
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(CommonError::UnexpectedError(anyhow::anyhow!(
|
||||
"获取房间数据失败"
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use crate::AppData;
|
||||
use chrono::{Local};
|
||||
use entity::im_user;
|
||||
use entity::prelude::ImUserEntity;
|
||||
use log::{debug, info};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::ColumnTrait;
|
||||
use sea_orm::EntityTrait;
|
||||
use sea_orm::IntoActiveModel;
|
||||
use sea_orm::QueryFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use tauri::State;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserInfo {
|
||||
uid: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_user_info(user_info: UserInfo, state: State<'_, AppData>) -> Result<(), String> {
|
||||
let db = state.db_conn.clone();
|
||||
|
||||
// 检查用户是否存在
|
||||
let exists = ImUserEntity::find()
|
||||
.filter(im_user::Column::Id.eq(&user_info.uid))
|
||||
.one(db.deref())
|
||||
.await
|
||||
.map_err(|err| format!("查询用户失败: {}", err))?;
|
||||
|
||||
if exists.is_none() {
|
||||
info!("用户不存在,准备插入新用户");
|
||||
|
||||
let user = im_user::ActiveModel {
|
||||
id: Set(user_info.uid.clone()),
|
||||
// TODO 这里先设置为 true,后续需要根据配置调整
|
||||
is_init: Set(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
im_user::Entity::insert(user)
|
||||
.exec(db.deref())
|
||||
.await
|
||||
.map_err(|err| format!("插入用户失败: {}", err))?;
|
||||
} else {
|
||||
debug!("用户已存在,无需插入");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_user_last_opt_time(
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<(), String> {
|
||||
info!("更新用户最后操作时间");
|
||||
let db = state.db_conn.clone();
|
||||
|
||||
let uid = state.user_info.lock().await.uid.clone();
|
||||
|
||||
// 检查用户是否存在
|
||||
let user = ImUserEntity::find()
|
||||
.filter(im_user::Column::Id.eq(uid.clone()))
|
||||
.one(db.deref())
|
||||
.await
|
||||
.map_err(|err| format!("查询用户失败: {}", err))?;
|
||||
|
||||
if let Some(user) = user {
|
||||
let mut active_model = user.into_active_model();
|
||||
active_model.last_opt_time = Set(Some(Local::now().timestamp_millis()));
|
||||
|
||||
ImUserEntity::update(active_model)
|
||||
.exec(db.deref())
|
||||
.await
|
||||
.map_err(|err| format!("更新用户最后操作时间失败: {}", err))?;
|
||||
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use crate::error::CommonError;
|
||||
use anyhow::Context;
|
||||
use sea_orm::{Database, DatabaseConnection};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct Settings {
|
||||
pub database: DatabaseSettings,
|
||||
pub backend: BackendSettings,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct DatabaseSettings {
|
||||
pub sqlite_file: String,
|
||||
}
|
||||
|
||||
impl DatabaseSettings {
|
||||
pub async fn connection_string(&self) -> Result<DatabaseConnection, CommonError> {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); // 项目根目录
|
||||
path.push("db.sqlite");
|
||||
let db_url = format!("sqlite:{}?mode=rwc", path.display());
|
||||
let db: DatabaseConnection = Database::connect(db_url)
|
||||
.await
|
||||
.with_context(|| "连接数据库异常")?;
|
||||
Ok(db)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct BackendSettings {
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
|
||||
let base_path = std::env::current_dir().expect("无法获取当前项目目录");
|
||||
let configuration_directory = base_path.join("configuration");
|
||||
|
||||
// Detect the running environment.
|
||||
// Default to `local` if unspecified.
|
||||
let environment: Environment = std::env::var("APP_ENVIRONMENT")
|
||||
.unwrap_or_else(|_| "local".into())
|
||||
.try_into()
|
||||
.expect("解析APP_ENVIRONMENT失败");
|
||||
|
||||
let environment_filename = format!("{}.yaml", environment.as_str());
|
||||
let settings = config::Config::builder()
|
||||
.add_source(config::File::from(
|
||||
configuration_directory.join("base.yaml"),
|
||||
))
|
||||
.add_source(config::File::from(
|
||||
configuration_directory.join(environment_filename),
|
||||
))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
settings.try_deserialize::<Settings>()
|
||||
}
|
||||
|
||||
/// The possible runtime environment for our application.
|
||||
#[derive(Debug)]
|
||||
pub enum Environment {
|
||||
Local,
|
||||
Production,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Environment::Local => "local",
|
||||
Environment::Production => "production",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Environment {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"local" => Ok(Self::Local),
|
||||
"production" => Ok(Self::Production),
|
||||
other => Err(format!(
|
||||
"{} is not a supported environment. Use either `local` or `production`.",
|
||||
other
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,4 @@ pub fn handle_app_event<R: Runtime>(app_handle: &AppHandle<R>, event: RunEvent)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ use screenshots::Screen;
|
||||
use serde::Serialize;
|
||||
use std::cmp;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{
|
||||
Arc, RwLock,
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tauri::path::BaseDirectory;
|
||||
@@ -23,7 +21,6 @@ pub struct DiskInfo {
|
||||
usage_percentage: f64,
|
||||
}
|
||||
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(deprecated)]
|
||||
use cocoa::appkit::NSWindow;
|
||||
@@ -283,9 +280,3 @@ pub async fn get_files_meta(files_path: Vec<String>) -> Result<Vec<FileMeta>, St
|
||||
|
||||
Ok(files_meta)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -52,22 +52,25 @@ async fn get_directory_size_with_progress(
|
||||
|
||||
// 创建取消接收器
|
||||
let mut cancel_receiver = CANCEL_SENDER.subscribe();
|
||||
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
|
||||
// 1.快速预扫描计算总文件数
|
||||
let mut total_files = 0u64;
|
||||
let mut entries = AsyncWalkDir::new(&path);
|
||||
|
||||
let _ = handle.emit("directory-scan-progress", &DirectoryScanProgress {
|
||||
current_path: "正在统计文件数量...".to_string(),
|
||||
files_processed: 0,
|
||||
total_size: 0,
|
||||
elapsed_time: 0,
|
||||
elapsed_seconds: 0.0,
|
||||
progress_percentage: 0.0,
|
||||
});
|
||||
|
||||
|
||||
let _ = handle.emit(
|
||||
"directory-scan-progress",
|
||||
&DirectoryScanProgress {
|
||||
current_path: "正在统计文件数量...".to_string(),
|
||||
files_processed: 0,
|
||||
total_size: 0,
|
||||
elapsed_time: 0,
|
||||
elapsed_seconds: 0.0,
|
||||
progress_percentage: 0.0,
|
||||
},
|
||||
);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 检查取消信号
|
||||
@@ -89,12 +92,12 @@ async fn get_directory_size_with_progress(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 2.实际扫描并计算准确进度
|
||||
let mut total_size = 0u64;
|
||||
let mut files_processed = 0u64;
|
||||
let mut last_progress_time = Instant::now();
|
||||
|
||||
|
||||
let mut entries = AsyncWalkDir::new(&path);
|
||||
|
||||
loop {
|
||||
@@ -133,7 +136,7 @@ async fn get_directory_size_with_progress(
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
|
||||
let progress = DirectoryScanProgress {
|
||||
current_path: current_path.clone(),
|
||||
files_processed,
|
||||
@@ -244,4 +247,4 @@ pub async fn get_directory_usage_info_with_progress(
|
||||
disk_usage_percentage,
|
||||
usage_percentage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::{Manager, Runtime, WindowEvent};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_log::fern::colors::{Color, ColoredLevelConfig};
|
||||
use tauri_plugin_log::{Target, TargetKind, WEBVIEW_TARGET};
|
||||
|
||||
pub trait CustomInit {
|
||||
fn init_plugin(self) -> Self;
|
||||
|
||||
@@ -11,7 +17,7 @@ pub trait CustomInit {
|
||||
impl<R: Runtime> CustomInit for tauri::Builder<R> {
|
||||
// 初始化插件
|
||||
fn init_plugin(self) -> Self {
|
||||
self.plugin(tauri_plugin_os::init())
|
||||
let mut builder = self.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
@@ -41,6 +47,14 @@ impl<R: Runtime> CustomInit for tauri::Builder<R> {
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_mic_recorder::init())
|
||||
.plugin(build_log_plugin());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_devtools::init());
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
// 初始化web窗口事件
|
||||
@@ -100,3 +114,43 @@ impl<R: Runtime> CustomInit for tauri::Builder<R> {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_log_plugin<R: Runtime>() -> TauriPlugin<R> {
|
||||
// 获取当前工作目录并创建 logs 子目录
|
||||
let mut log_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
log_dir.push("logs");
|
||||
|
||||
// 确保 logs 目录存在
|
||||
if !log_dir.exists() {
|
||||
std::fs::create_dir_all(&log_dir).unwrap_or_else(|e| {
|
||||
eprintln!("Failed to create logs directory: {}", e);
|
||||
});
|
||||
}
|
||||
|
||||
tauri_plugin_log::Builder::new()
|
||||
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
|
||||
.skip_logger()
|
||||
.level(log::LevelFilter::Info)
|
||||
.level_for("sqlx::query", log::LevelFilter::Warn)
|
||||
.level_for("hula_app_lib", log::LevelFilter::Debug)
|
||||
.targets([
|
||||
Target::new(TargetKind::Stdout),
|
||||
// 将 rust 日志打印到 webview的 devtool 中
|
||||
Target::new(TargetKind::Webview),
|
||||
// 将日志保存到项目的 logs 目录下
|
||||
Target::new(TargetKind::Folder {
|
||||
path: log_dir,
|
||||
file_name: Some("app".into()),
|
||||
})
|
||||
.filter(|metadata| !metadata.target().starts_with(WEBVIEW_TARGET)),
|
||||
])
|
||||
.with_colors(ColoredLevelConfig {
|
||||
error: Color::Red,
|
||||
warn: Color::Yellow,
|
||||
debug: Color::White,
|
||||
info: Color::Green,
|
||||
trace: Color::White,
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||
let _ = TrayIconBuilder::with_id("tray")
|
||||
.tooltip("HuLa")
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.menu(&tray_menu) // 直接设置菜单,让系统处理右键显示
|
||||
.show_menu_on_left_click(false) // 禁用左键显示菜单
|
||||
.menu(&tray_menu) // 直接设置菜单,让系统处理右键显示
|
||||
.show_menu_on_left_click(false) // 禁用左键显示菜单
|
||||
.on_menu_event(move |app, event| {
|
||||
let id = event.id();
|
||||
if id == &open_id {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#[derive(thiserror::Error)]
|
||||
pub enum CommonError {
|
||||
#[error(transparent)]
|
||||
UnexpectedError(#[from] anyhow::Error),
|
||||
#[error("Database error: {0}")]
|
||||
DatabaseError(#[from] sea_orm::DbErr),
|
||||
#[error("token过期")]
|
||||
TokenExpired,
|
||||
}
|
||||
|
||||
impl From<CommonError> for String {
|
||||
fn from(err: CommonError) -> String {
|
||||
err.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CommonError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
error_chain_fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error_chain_fmt(
|
||||
e: &impl std::error::Error,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
writeln!(f, "{}\n", e)?;
|
||||
let mut current = e.source();
|
||||
while let Some(cause) = current {
|
||||
writeln!(f, "Caused by:\n\t{}", cause)?;
|
||||
current = cause.source();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
use crate::error::CommonError;
|
||||
use crate::pojo::common::ApiResult;
|
||||
use anyhow::Context;
|
||||
use log::{debug, error, info, warn};
|
||||
use reqwest::{Client, Method, RequestBuilder, header};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
/// 智能 HTTP 客户端,支持自动 token 管理和过期重试
|
||||
pub struct ImRequestClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
pub token: Arc<Mutex<Option<String>>>,
|
||||
pub refresh_token: Arc<Mutex<Option<String>>>,
|
||||
app_handle: Arc<Mutex<Option<AppHandle>>>,
|
||||
}
|
||||
|
||||
impl ImRequestClient {
|
||||
/// 创建新的请求客户端
|
||||
pub async fn new(base_url: String) -> Result<Self, CommonError> {
|
||||
let client = Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.with_context(|| "创建 HTTP 客户端失败")?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
base_url,
|
||||
token: Arc::new(Mutex::new(None)),
|
||||
refresh_token: Arc::new(Mutex::new(None)),
|
||||
app_handle: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
/// 设置 AppHandle
|
||||
pub fn set_app_handle(&self, app_handle: AppHandle) {
|
||||
if let Ok(mut handle) = self.app_handle.lock() {
|
||||
*handle = Some(app_handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送 GET 请求
|
||||
pub fn get(&self, path: &str) -> RequestBuilderWrapper {
|
||||
self.request(Method::GET, path)
|
||||
}
|
||||
|
||||
/// 发送 POST 请求
|
||||
pub fn post(&self, path: &str) -> RequestBuilderWrapper {
|
||||
self.request(Method::POST, path)
|
||||
}
|
||||
|
||||
/// 发送 PUT 请求
|
||||
pub fn put(&self, path: &str) -> RequestBuilderWrapper {
|
||||
self.request(Method::PUT, path)
|
||||
}
|
||||
|
||||
/// 发送 DELETE 请求
|
||||
pub fn delete(&self, path: &str) -> RequestBuilderWrapper {
|
||||
self.request(Method::DELETE, path)
|
||||
}
|
||||
|
||||
/// 创建请求构建器
|
||||
fn request(&self, method: Method, path: &str) -> RequestBuilderWrapper {
|
||||
let url = if path.starts_with("http") {
|
||||
path.to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}/{}",
|
||||
self.base_url.trim_end_matches('/'),
|
||||
path.trim_start_matches('/')
|
||||
)
|
||||
};
|
||||
|
||||
let request_builder = self.client.request(method.clone(), &url);
|
||||
|
||||
// 注意:这里不能直接获取 token,因为这是同步方法
|
||||
// token 将在发送请求时异步获取
|
||||
|
||||
RequestBuilderWrapper {
|
||||
request_builder,
|
||||
client: self,
|
||||
url,
|
||||
method,
|
||||
json_body: None,
|
||||
headers: Vec::new(),
|
||||
query_params: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新 token
|
||||
pub async fn refresh_token(&self) -> Result<(), CommonError> {
|
||||
let current_refresh_token = {
|
||||
let refresh_token_guard = self
|
||||
.refresh_token
|
||||
.lock()
|
||||
.expect("获取 refresh_token 锁失败");
|
||||
refresh_token_guard.clone()
|
||||
};
|
||||
info!("开始刷新 token...");
|
||||
// 构建刷新 token 的请求
|
||||
let refresh_url = format!("{}/token/refreshToken", self.base_url);
|
||||
let request_builder = self.client.request(Method::POST, &refresh_url);
|
||||
|
||||
// 发送请求
|
||||
let response = request_builder
|
||||
.json(&HashMap::from([(
|
||||
"refreshToken",
|
||||
current_refresh_token.unwrap(),
|
||||
)]))
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| "刷新 token 请求失败")?;
|
||||
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.with_context(|| "读取刷新 token 响应体失败")?;
|
||||
|
||||
// 解析响应
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RefreshTokenResponse {
|
||||
token: String,
|
||||
refresh_token: String,
|
||||
}
|
||||
|
||||
let result: ApiResult<RefreshTokenResponse> = match serde_json::from_str(&response_text) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("刷新 token JSON 解析错误: {}", e);
|
||||
error!("响应内容: {}", response_text);
|
||||
return Err(CommonError::UnexpectedError(anyhow::anyhow!(
|
||||
"解析刷新 token 响应失败: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
if !result.success {
|
||||
error!(
|
||||
"刷新 token 失败: {}",
|
||||
result.msg.clone().unwrap_or_default()
|
||||
);
|
||||
return Err(CommonError::UnexpectedError(anyhow::anyhow!(
|
||||
"刷新 token 失败!"
|
||||
)));
|
||||
}
|
||||
|
||||
// 更新 token 和 refresh_token
|
||||
if let Some(data) = result.data {
|
||||
info!("token 刷新成功");
|
||||
|
||||
// 更新 token
|
||||
if let Ok(mut token_guard) = self.token.lock() {
|
||||
*token_guard = Some(data.token);
|
||||
}
|
||||
|
||||
// 更新 refresh_token
|
||||
if let Ok(mut refresh_token_guard) = self.refresh_token.lock() {
|
||||
*refresh_token_guard = Some(data.refresh_token);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
error!("刷新 token 响应数据为空");
|
||||
Err(CommonError::UnexpectedError(anyhow::anyhow!(
|
||||
"刷新 token 响应数据为空"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 请求构建器包装器,支持自动重试
|
||||
pub struct RequestBuilderWrapper<'a> {
|
||||
request_builder: RequestBuilder,
|
||||
client: &'a ImRequestClient,
|
||||
url: String,
|
||||
method: Method,
|
||||
json_body: Option<String>,
|
||||
headers: Vec<(String, String)>,
|
||||
query_params: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> RequestBuilderWrapper<'a> {
|
||||
/// 添加查询参数
|
||||
pub fn query<T: serde::Serialize + ?Sized>(mut self, query: &T) -> Self {
|
||||
// 序列化查询参数以便重试时使用和日志记录
|
||||
if let Ok(query_string) = serde_json::to_string(query) {
|
||||
self.query_params = Some(query_string);
|
||||
}
|
||||
self.request_builder = self.request_builder.query(query);
|
||||
self
|
||||
}
|
||||
|
||||
/// 添加 JSON 请求体
|
||||
pub fn json<T: serde::Serialize + ?Sized>(mut self, json: &T) -> Self {
|
||||
// 序列化 JSON 体以便重试时使用
|
||||
if let Ok(json_string) = serde_json::to_string(json) {
|
||||
self.json_body = Some(json_string);
|
||||
}
|
||||
self.request_builder = self.request_builder.json(json);
|
||||
self
|
||||
}
|
||||
|
||||
/// 添加请求头
|
||||
pub fn header<K, V>(mut self, key: K, value: V) -> Self
|
||||
where
|
||||
header::HeaderName: TryFrom<K>,
|
||||
<header::HeaderName as TryFrom<K>>::Error: Into<http::Error>,
|
||||
header::HeaderValue: TryFrom<V>,
|
||||
<header::HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
|
||||
K: Clone,
|
||||
V: Clone,
|
||||
{
|
||||
// 记录请求头以便重试时使用
|
||||
if let (Ok(header_name), Ok(header_value)) = (
|
||||
header::HeaderName::try_from(key.clone()).map_err(|e| e.into()),
|
||||
header::HeaderValue::try_from(value.clone()).map_err(|e| e.into()),
|
||||
) {
|
||||
self.headers.push((
|
||||
header_name.to_string(),
|
||||
header_value.to_str().unwrap_or_default().to_string(),
|
||||
));
|
||||
}
|
||||
self.request_builder = self.request_builder.header(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// 发送请求并解析 JSON 响应,支持自动 token 刷新
|
||||
pub async fn send_json<T: serde::de::DeserializeOwned>(
|
||||
mut self,
|
||||
) -> Result<ApiResult<T>, CommonError> {
|
||||
// 记录请求信息
|
||||
let mut log_message = format!("发送请求到: {} [{}]", self.url, self.method);
|
||||
|
||||
// 添加查询参数信息
|
||||
if let Some(ref query_params) = self.query_params {
|
||||
log_message.push_str(&format!(" 查询参数: {}", query_params));
|
||||
}
|
||||
|
||||
// 添加请求体信息
|
||||
if let Some(ref json_body) = self.json_body {
|
||||
log_message.push_str(&format!(" 请求体: {}", json_body));
|
||||
}
|
||||
|
||||
debug!("{}", log_message);
|
||||
|
||||
// 获取 token 并添加到请求头
|
||||
let current_token = {
|
||||
if let Ok(token_guard) = self.client.token.lock() {
|
||||
token_guard.clone()
|
||||
} else {
|
||||
warn!("获取 token 锁失败");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(token) = ¤t_token {
|
||||
self.request_builder = self
|
||||
.request_builder
|
||||
.header(header::AUTHORIZATION, format!("Bearer {}", token));
|
||||
} else {
|
||||
warn!("没有设置 token");
|
||||
}
|
||||
|
||||
let response = self
|
||||
.request_builder
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("[{}:{}] 发送请求失败: {}", file!(), line!(), self.url))?;
|
||||
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.with_context(|| format!("[{}:{}] 读取响应体失败", file!(), line!()))?;
|
||||
|
||||
debug!("开始解析响应");
|
||||
// 解析为目标类型
|
||||
let result: ApiResult<T> = match serde_json::from_str(&response_text) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("JSON 解析错误: {}", e);
|
||||
// error!("响应内容: {}", response_text);
|
||||
return Err(CommonError::UnexpectedError(anyhow::anyhow!(
|
||||
"解析 JSON 失败: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// token 过期,尝试刷新 token 并重试
|
||||
if result.code == Some(40004) {
|
||||
info!("检测到 token 过期,尝试刷新 token");
|
||||
|
||||
// 尝试刷新 token
|
||||
match self.client.refresh_token().await {
|
||||
Ok(()) => {
|
||||
info!("token 刷新成功,重新发送请求");
|
||||
|
||||
// 重新构建请求(因为原来的 request_builder 已经被消费了)
|
||||
let mut new_request_builder =
|
||||
self.client.client.request(self.method.clone(), &self.url);
|
||||
|
||||
// 添加保存的 JSON 体
|
||||
if let Some(json_body) = &self.json_body {
|
||||
new_request_builder = new_request_builder
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(json_body.clone());
|
||||
}
|
||||
|
||||
// 添加保存的请求头
|
||||
for (key, value) in &self.headers {
|
||||
new_request_builder = new_request_builder.header(key, value);
|
||||
}
|
||||
|
||||
// 获取新的 token
|
||||
let new_token = {
|
||||
if let Ok(token_guard) = self.client.token.lock() {
|
||||
token_guard.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(token) = &new_token {
|
||||
new_request_builder = new_request_builder
|
||||
.header(header::AUTHORIZATION, format!("Bearer {}", token));
|
||||
}
|
||||
|
||||
// 重新发送请求
|
||||
info!("重试请求到: {}", self.url);
|
||||
let retry_response = new_request_builder.send().await.with_context(|| {
|
||||
format!("[{}:{}] 重试请求失败: {}", file!(), line!(), self.url)
|
||||
})?;
|
||||
|
||||
let retry_response_text = retry_response
|
||||
.text()
|
||||
.await
|
||||
.with_context(|| format!("[{}:{}] 读取重试响应体失败", file!(), line!()))?;
|
||||
|
||||
// 解析重试响应
|
||||
let retry_result: ApiResult<T> =
|
||||
match serde_json::from_str(&retry_response_text) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("重试请求 JSON 解析错误: {}", e);
|
||||
// error!("响应内容: {}", retry_response_text);
|
||||
return Err(CommonError::UnexpectedError(anyhow::anyhow!(
|
||||
"解析重试请求 JSON 失败: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
if !&retry_result.success {
|
||||
error!(
|
||||
"重试请求失败: {}",
|
||||
&retry_result.msg.clone().unwrap_or_default()
|
||||
);
|
||||
return Err(CommonError::UnexpectedError(anyhow::anyhow!(
|
||||
"重试请求失败!"
|
||||
)));
|
||||
}
|
||||
|
||||
debug!("重试请求成功");
|
||||
|
||||
// 发送 token_expired 事件到前端,包含当前的 token 和 refreshToken
|
||||
if let Ok(app_handle_guard) = self.client.app_handle.lock() {
|
||||
if let Some(app_handle) = app_handle_guard.as_ref() {
|
||||
let token_info = {
|
||||
let current_token =
|
||||
if let Ok(token_guard) = self.client.token.lock() {
|
||||
token_guard.clone()
|
||||
} else {
|
||||
warn!("获取 token 锁失败");
|
||||
None
|
||||
};
|
||||
let current_refresh_token = if let Ok(refresh_token_guard) =
|
||||
self.client.refresh_token.lock()
|
||||
{
|
||||
refresh_token_guard.clone()
|
||||
} else {
|
||||
warn!("获取 refresh_token 锁失败");
|
||||
None
|
||||
};
|
||||
serde_json::json!({
|
||||
"token": current_token,
|
||||
"refreshToken": current_refresh_token
|
||||
})
|
||||
};
|
||||
|
||||
if let Err(emit_err) =
|
||||
app_handle.emit("refresh_token_event", token_info)
|
||||
{
|
||||
error!("发送 refresh_token 事件失败: {:?}", emit_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(retry_result);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("刷新 token 失败: {:?}", e);
|
||||
return Err(CommonError::TokenExpired);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !&result.success {
|
||||
error!("请求失败: {}", &result.msg.clone().unwrap_or_default());
|
||||
return Err(CommonError::UnexpectedError(anyhow::anyhow!("请求失败!")));
|
||||
}
|
||||
|
||||
debug!("解析完成");
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
+159
-5
@@ -16,19 +16,54 @@ use desktops::{common_cmd, directory_scanner, init, tray, video_thumbnail::get_v
|
||||
use directory_scanner::{cancel_directory_scan, get_directory_usage_info_with_progress};
|
||||
#[cfg(desktop)]
|
||||
use init::CustomInit;
|
||||
use moka::future::Cache;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
pub mod command;
|
||||
pub mod configuration;
|
||||
pub mod error;
|
||||
pub mod im_reqest_client;
|
||||
pub mod pojo;
|
||||
pub mod repository;
|
||||
mod vo;
|
||||
|
||||
use crate::command::room_member_command::{
|
||||
cursor_page_room_members, get_room_members, page_room, update_my_room_info,
|
||||
};
|
||||
use crate::configuration::get_configuration;
|
||||
use crate::error::CommonError;
|
||||
use crate::im_reqest_client::ImRequestClient;
|
||||
use anyhow::Context;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
|
||||
// 移动端依赖
|
||||
#[cfg(mobile)]
|
||||
mod mobiles;
|
||||
#[cfg(mobile)]
|
||||
use init::CustomInit;
|
||||
#[cfg(mobile)]
|
||||
use mobiles::init;
|
||||
#[cfg(mobile)]
|
||||
mod mobiles;
|
||||
|
||||
pub fn run() {
|
||||
pub struct AppData {
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
request_client: Arc<Mutex<ImRequestClient>>,
|
||||
user_info: Arc<Mutex<UserInfo>>,
|
||||
cache: Cache<String, String>,
|
||||
}
|
||||
|
||||
use crate::command::contact_command::list_contacts_command;
|
||||
use crate::command::message_command::{
|
||||
check_user_init_and_fetch_messages, page_msg, save_msg, send_msg,
|
||||
};
|
||||
use tauri::Listener;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub async fn run() {
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
setup_desktop();
|
||||
setup_desktop().await.unwrap();
|
||||
}
|
||||
#[cfg(mobile)]
|
||||
{
|
||||
@@ -37,12 +72,57 @@ pub fn run() {
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn setup_desktop() {
|
||||
async fn setup_desktop() -> Result<(), CommonError> {
|
||||
use log::info;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
|
||||
use crate::command::user_command::{save_user_info, update_user_last_opt_time};
|
||||
|
||||
let configuration = Arc::new(get_configuration().expect("加载配置文件失败"));
|
||||
let db = Arc::new(configuration.database.connection_string().await?);
|
||||
|
||||
// 数据库迁移
|
||||
Migrator::up(db.as_ref(), None).await?;
|
||||
info!("数据库迁移完成");
|
||||
|
||||
let im_request_client =
|
||||
ImRequestClient::new(configuration.clone().backend.base_url.clone()).await?;
|
||||
let user_info = UserInfo {
|
||||
token: Default::default(),
|
||||
refresh_token: Default::default(),
|
||||
uid: Default::default(),
|
||||
};
|
||||
|
||||
// 创建一个缓存实例
|
||||
let cache: Cache<String, String> = Cache::builder()
|
||||
// Time to idle (TTI): 30 minutes
|
||||
.time_to_idle(Duration::from_secs(30 * 60))
|
||||
// Create the cache.
|
||||
.build();
|
||||
|
||||
let client = Arc::new(Mutex::new(im_request_client));
|
||||
let user_info = Arc::new(Mutex::new(user_info));
|
||||
tauri::Builder::default()
|
||||
.init_plugin()
|
||||
.init_webwindow_event()
|
||||
.init_window_event()
|
||||
.manage(AppData {
|
||||
db_conn: db.clone(),
|
||||
request_client: client.clone(),
|
||||
user_info: user_info.clone(),
|
||||
cache,
|
||||
})
|
||||
.setup(move |app| {
|
||||
// 设置 AppHandle 到 ImRequestClient(使用 spawn 避免运行时嵌套)
|
||||
let client_clone = client.clone();
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let client_guard = client_clone.lock().await;
|
||||
client_guard.set_app_handle(app_handle);
|
||||
});
|
||||
|
||||
// 监听前端事件,保存登录用户信息
|
||||
setup_user_info_listener(app, client.clone(), user_info.clone(), db.clone());
|
||||
tray::create_tray(app.handle())?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -55,6 +135,16 @@ fn setup_desktop() {
|
||||
get_video_thumbnail,
|
||||
#[cfg(target_os = "macos")]
|
||||
hide_title_bar_buttons,
|
||||
save_user_info,
|
||||
update_user_last_opt_time,
|
||||
page_room,
|
||||
get_room_members,
|
||||
update_my_room_info,
|
||||
cursor_page_room_members,
|
||||
list_contacts_command,
|
||||
page_msg,
|
||||
send_msg,
|
||||
save_msg,
|
||||
push_window_payload,
|
||||
get_window_payload,
|
||||
get_files_meta,
|
||||
@@ -71,6 +161,70 @@ fn setup_desktop() {
|
||||
let _ = (app_handle, event);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 设置用户信息事件监听器
|
||||
fn setup_user_info_listener(
|
||||
app: &tauri::App,
|
||||
client: Arc<Mutex<ImRequestClient>>,
|
||||
user_info: Arc<Mutex<UserInfo>>,
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
) {
|
||||
app.listen("set_user_info", {
|
||||
let client = client.clone();
|
||||
let user_info = user_info.clone();
|
||||
let db_conn = db_conn.clone();
|
||||
move |event| {
|
||||
let client = client.clone();
|
||||
let user_info = user_info.clone();
|
||||
let db_conn = db_conn.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Ok(payload) = serde_json::from_str::<UserInfo>(&event.payload()) {
|
||||
let client = client.lock().await;
|
||||
|
||||
// 更新 client 的 token
|
||||
if let Ok(mut token_guard) = client.token.lock() {
|
||||
*token_guard = Some(payload.token.clone());
|
||||
}
|
||||
|
||||
if let Ok(mut refresh_token_guard) = client.refresh_token.lock() {
|
||||
*refresh_token_guard = Some(payload.refresh_token.clone());
|
||||
}
|
||||
|
||||
// 更新 client 的 refresh_token(如果有的话)
|
||||
// 这里假设 UserInfo 可能包含 refresh_token,如果没有则保持原值
|
||||
let mut user_info = user_info.lock().await;
|
||||
user_info.uid = payload.uid.clone();
|
||||
user_info.token = payload.token.clone();
|
||||
user_info.refresh_token = payload.refresh_token.clone();
|
||||
|
||||
// 检查用户的 is_init 状态并获取消息
|
||||
if let Err(e) =
|
||||
check_user_init_and_fetch_messages(&client, db_conn.deref(), &payload.uid)
|
||||
.await
|
||||
{
|
||||
log::error!("检查用户初始化状态并获取消息失败: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserInfo {
|
||||
pub token: String,
|
||||
pub refresh_token: String,
|
||||
pub uid: String,
|
||||
}
|
||||
|
||||
pub async fn build_request_client() -> Result<reqwest::Client, CommonError> {
|
||||
let client = reqwest::Client::builder()
|
||||
.build()
|
||||
.with_context(|| "Reqwest client 异常")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
hula_app_lib::run()
|
||||
#[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
hula_app_lib::run().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub mod init;
|
||||
pub mod init;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PageParam {
|
||||
pub current: u32,
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CursorPageParam {
|
||||
pub page_size: u32,
|
||||
pub cursor: String,
|
||||
pub create_id: Option<String>,
|
||||
pub create_time: Option<i64>,
|
||||
pub update_time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CursorPageResp<T> {
|
||||
pub cursor: String,
|
||||
pub is_last: bool,
|
||||
pub list: Option<T>,
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginParam {
|
||||
pub account: String,
|
||||
pub password: String,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiResult<T> {
|
||||
pub success: bool,
|
||||
pub code: Option<i32>,
|
||||
pub msg: Option<String>,
|
||||
pub tid: String,
|
||||
pub version: String,
|
||||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Page<T> {
|
||||
pub records: Vec<T>,
|
||||
pub total: String,
|
||||
pub size: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginResp {
|
||||
pub uuid: Option<String>,
|
||||
pub token: String,
|
||||
pub refresh_token: String,
|
||||
pub client: String,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod common;
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::error::CommonError;
|
||||
use anyhow::Context;
|
||||
use entity::im_config;
|
||||
use sea_orm::QueryFilter;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, IntoActiveModel, Set};
|
||||
use sea_orm::{DatabaseConnection, EntityTrait, TransactionTrait};
|
||||
|
||||
/// 获取配置列表
|
||||
pub async fn list_config(
|
||||
db: &DatabaseConnection,
|
||||
login_uid: &str,
|
||||
) -> Result<Vec<im_config::Model>, CommonError> {
|
||||
let list = im_config::Entity::find()
|
||||
.filter(im_config::Column::LoginUid.eq(login_uid))
|
||||
.all(db)
|
||||
.await?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// 根据配置键获取配置值
|
||||
pub async fn get_config_by_key(
|
||||
db: &DatabaseConnection,
|
||||
config_key: &str,
|
||||
login_uid: &str,
|
||||
) -> Result<Option<im_config::Model>, CommonError> {
|
||||
let config = im_config::Entity::find()
|
||||
.filter(im_config::Column::ConfigKey.eq(config_key))
|
||||
.filter(im_config::Column::LoginUid.eq(login_uid))
|
||||
.one(db)
|
||||
.await
|
||||
.with_context(|| "查询配置失败")?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// 保存或更新配置
|
||||
pub async fn save_or_update_config(
|
||||
db: &DatabaseConnection,
|
||||
config_key: &str,
|
||||
config_value: Option<String>,
|
||||
login_uid: &str,
|
||||
) -> Result<(), CommonError> {
|
||||
// 查找现有配置
|
||||
let existing_config = get_config_by_key(db, config_key, login_uid).await?;
|
||||
|
||||
if let Some(config) = existing_config {
|
||||
// 更新现有配置
|
||||
let mut config_active = config.into_active_model();
|
||||
config_active.config_value = Set(config_value);
|
||||
config_active.update(db).await?;
|
||||
} else {
|
||||
// 创建新配置
|
||||
let new_config = im_config::Model {
|
||||
id: 0, // 自增主键
|
||||
config_key: config_key.to_string(),
|
||||
config_value,
|
||||
login_uid: login_uid.to_string(),
|
||||
};
|
||||
let mut config_active = new_config.into_active_model();
|
||||
config_active.id = sea_orm::NotSet;
|
||||
config_active.insert(db).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 批量保存配置
|
||||
pub async fn save_config_batch(
|
||||
db: &DatabaseConnection,
|
||||
configs: Vec<im_config::Model>,
|
||||
login_uid: &str,
|
||||
) -> Result<(), CommonError> {
|
||||
if configs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 使用事务确保操作的原子性
|
||||
let txn = db.begin().await?;
|
||||
|
||||
// 先删除当前用户的现有配置数据
|
||||
im_config::Entity::delete_many()
|
||||
.filter(im_config::Column::LoginUid.eq(login_uid))
|
||||
.exec(&txn)
|
||||
.await
|
||||
.with_context(|| "删除现有配置数据失败")?;
|
||||
|
||||
// 批量插入新的配置数据
|
||||
let active_models: Vec<im_config::ActiveModel> = configs
|
||||
.into_iter()
|
||||
.map(|mut config| {
|
||||
config.login_uid = login_uid.to_string();
|
||||
let mut active_model = config.into_active_model();
|
||||
active_model.id = sea_orm::NotSet;
|
||||
active_model
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !active_models.is_empty() {
|
||||
im_config::Entity::insert_many(active_models)
|
||||
.exec(&txn)
|
||||
.await
|
||||
.with_context(|| "批量插入配置数据失败")?;
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
txn.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除配置
|
||||
pub async fn delete_config(
|
||||
db: &DatabaseConnection,
|
||||
config_key: &str,
|
||||
login_uid: &str,
|
||||
) -> Result<(), CommonError> {
|
||||
im_config::Entity::delete_many()
|
||||
.filter(im_config::Column::ConfigKey.eq(config_key))
|
||||
.filter(im_config::Column::LoginUid.eq(login_uid))
|
||||
.exec(db)
|
||||
.await
|
||||
.with_context(|| "删除配置失败")?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::error::CommonError;
|
||||
use anyhow::Context;
|
||||
use entity::im_contact;
|
||||
use sea_orm::{
|
||||
ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter, TransactionTrait,
|
||||
};
|
||||
|
||||
pub async fn list_contact(
|
||||
db: &DatabaseConnection,
|
||||
login_uid: &str,
|
||||
) -> Result<Vec<im_contact::Model>, CommonError> {
|
||||
let list = im_contact::Entity::find()
|
||||
.filter(im_contact::Column::LoginUid.eq(login_uid))
|
||||
.all(db)
|
||||
.await?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// 批量保存会话数据到本地数据库
|
||||
pub async fn save_contact_batch(
|
||||
db: &DatabaseConnection,
|
||||
contacts: Vec<im_contact::Model>,
|
||||
login_uid: &str,
|
||||
) -> Result<(), CommonError> {
|
||||
if contacts.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 使用事务确保操作的原子性
|
||||
let txn = db.begin().await?;
|
||||
|
||||
// 先删除当前用户的现有会话数据
|
||||
im_contact::Entity::delete_many()
|
||||
.filter(im_contact::Column::LoginUid.eq(login_uid))
|
||||
.exec(&txn)
|
||||
.await
|
||||
.with_context(|| "删除现有会话数据失败")?;
|
||||
|
||||
// 批量插入新的会话数据
|
||||
let active_models: Vec<im_contact::ActiveModel> = contacts
|
||||
.into_iter()
|
||||
.map(|mut contact| {
|
||||
contact.login_uid = login_uid.to_string();
|
||||
let active_model = contact.into_active_model();
|
||||
active_model
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !active_models.is_empty() {
|
||||
im_contact::Entity::insert_many(active_models)
|
||||
.exec(&txn)
|
||||
.await
|
||||
.with_context(|| "批量插入会话数据失败")?;
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
txn.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use crate::error::CommonError;
|
||||
use crate::pojo::common::{CursorPageParam, CursorPageResp};
|
||||
use anyhow::Context;
|
||||
use entity::im_message;
|
||||
use log::{debug, info};
|
||||
use sea_orm::{
|
||||
ColumnTrait, ConnectionTrait, DatabaseConnection, DatabaseTransaction, EntityTrait,
|
||||
IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set,
|
||||
};
|
||||
|
||||
pub async fn save_all<C>(db: &C, messages: Vec<im_message::Model>) -> Result<(), CommonError>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
// SQLite 的变量限制通常是 999,为了安全起见,我们设置批次大小为 100
|
||||
// 每个消息大约有 10-15 个字段,所以 100 条消息大约使用 1000-1500 个变量
|
||||
const BATCH_SIZE: usize = 100;
|
||||
|
||||
let active_models: Vec<im_message::ActiveModel> = messages
|
||||
.into_iter()
|
||||
.map(|message| {
|
||||
let msg_active = message.into_active_model();
|
||||
msg_active
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 如果数据量小于批次大小,直接插入
|
||||
if active_models.len() <= BATCH_SIZE {
|
||||
if !active_models.is_empty() {
|
||||
let count = active_models.len();
|
||||
im_message::Entity::insert_many(active_models)
|
||||
.exec(db)
|
||||
.await
|
||||
.with_context(|| "批量插入消息失败")?;
|
||||
info!("消息插入完成,共 {} 条", count);
|
||||
}
|
||||
} else {
|
||||
// 分批插入
|
||||
for (batch_index, chunk) in active_models.chunks(BATCH_SIZE).enumerate() {
|
||||
debug!(
|
||||
"正在插入第 {} 批消息,共 {} 条",
|
||||
batch_index + 1,
|
||||
chunk.len()
|
||||
);
|
||||
|
||||
im_message::Entity::insert_many(chunk.to_vec())
|
||||
.exec(db)
|
||||
.await
|
||||
.with_context(|| format!("插入第 {} 批消息失败", batch_index + 1))?;
|
||||
}
|
||||
|
||||
info!("所有消息批量插入完成,总计 {} 条", active_models.len());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 根据房间ID进行游标分页查询消息
|
||||
pub async fn cursor_page_messages(
|
||||
db: &DatabaseConnection,
|
||||
room_id: String,
|
||||
cursor_page_param: CursorPageParam,
|
||||
login_uid: &str,
|
||||
) -> Result<CursorPageResp<Vec<im_message::Model>>, CommonError> {
|
||||
// 查询总数
|
||||
let total = im_message::Entity::find()
|
||||
.filter(im_message::Column::RoomId.eq(&room_id))
|
||||
.filter(im_message::Column::LoginUid.eq(login_uid))
|
||||
.count(db)
|
||||
.await
|
||||
.with_context(|| "查询消息总数失败")?;
|
||||
|
||||
let mut query = im_message::Entity::find()
|
||||
.filter(im_message::Column::RoomId.eq(room_id))
|
||||
.filter(im_message::Column::LoginUid.eq(login_uid))
|
||||
.order_by_desc(im_message::Column::Id)
|
||||
.limit(cursor_page_param.page_size as u64);
|
||||
|
||||
// 如果提供了游标,添加过滤条件
|
||||
if !cursor_page_param.cursor.is_empty() {
|
||||
// 使用游标值过滤,获取小于该ID的记录(因为是降序排列)
|
||||
query = query.filter(im_message::Column::Id.lt(&cursor_page_param.cursor));
|
||||
}
|
||||
|
||||
let messages = query.all(db).await.with_context(|| "查询消息列表失败")?;
|
||||
|
||||
// 生成下一页的游标
|
||||
let next_cursor = if messages.len() < cursor_page_param.page_size as usize {
|
||||
String::new() // 已经是最后一页
|
||||
} else {
|
||||
messages
|
||||
.last()
|
||||
.map(|msg| msg.id.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let is_last = messages.len() < cursor_page_param.page_size as usize;
|
||||
|
||||
Ok(CursorPageResp {
|
||||
cursor: next_cursor,
|
||||
is_last,
|
||||
list: Some(messages),
|
||||
total,
|
||||
})
|
||||
}
|
||||
|
||||
/// 保存单个消息到数据库
|
||||
pub async fn save_message(
|
||||
db: &DatabaseTransaction,
|
||||
message: im_message::Model,
|
||||
) -> Result<(), CommonError> {
|
||||
let active_model = message.into_active_model();
|
||||
im_message::Entity::insert(active_model).exec(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新消息发送状态
|
||||
pub async fn update_message_status(
|
||||
db: &DatabaseConnection,
|
||||
message_id: &str,
|
||||
status: &str,
|
||||
id: Option<String>,
|
||||
login_uid: String,
|
||||
) -> Result<(), CommonError> {
|
||||
let mut active_model: im_message::ActiveModel =
|
||||
im_message::Entity::find_by_id((message_id.to_string(), login_uid))
|
||||
.one(db)
|
||||
.await
|
||||
.with_context(|| "查找消息失败")?
|
||||
.ok_or_else(|| CommonError::UnexpectedError(anyhow::anyhow!("消息不存在")))?
|
||||
.into_active_model();
|
||||
|
||||
active_model.send_status = Set(status.to_string());
|
||||
|
||||
if status == "success" {
|
||||
active_model.id = Set(id.unwrap());
|
||||
}
|
||||
|
||||
im_message::Entity::update_many()
|
||||
.set(active_model)
|
||||
.filter(im_message::Column::Id.eq(message_id))
|
||||
.exec(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
use anyhow::Context;
|
||||
use entity::{im_room, im_room_member};
|
||||
use log::{debug, info};
|
||||
use sea_orm::EntityTrait;
|
||||
use sea_orm::IntoActiveModel;
|
||||
use sea_orm::PaginatorTrait;
|
||||
use sea_orm::QuerySelect;
|
||||
use sea_orm::TransactionTrait;
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, QueryFilter, QueryOrder};
|
||||
|
||||
use crate::pojo::common::{CursorPageParam, CursorPageResp};
|
||||
use crate::{
|
||||
error::CommonError,
|
||||
pojo::common::{Page, PageParam},
|
||||
};
|
||||
|
||||
pub async fn cursor_page_room_members(
|
||||
db: &DatabaseConnection,
|
||||
room_id: String,
|
||||
cursor_page_param: CursorPageParam,
|
||||
login_uid: &str,
|
||||
) -> Result<CursorPageResp<Vec<im_room_member::Model>>, CommonError> {
|
||||
// 查询总数
|
||||
let total = im_room_member::Entity::find()
|
||||
.filter(im_room_member::Column::RoomId.eq(&room_id))
|
||||
.filter(im_room_member::Column::LoginUid.eq(login_uid))
|
||||
.count(db)
|
||||
.await
|
||||
.with_context(|| "查询房间成员总数失败")?;
|
||||
|
||||
let mut query = im_room_member::Entity::find()
|
||||
.filter(im_room_member::Column::RoomId.eq(room_id))
|
||||
.filter(im_room_member::Column::LoginUid.eq(login_uid))
|
||||
.order_by_desc(im_room_member::Column::LastOptTime)
|
||||
.limit(cursor_page_param.page_size as u64);
|
||||
|
||||
// 如果提供了游标,解析游标值并添加过滤条件
|
||||
if !cursor_page_param.cursor.is_empty() {
|
||||
// 从 cursor 中根据'_'分割最后一个字符串转为 i64
|
||||
let cursor_parts: Vec<&str> = cursor_page_param.cursor.split('_').collect();
|
||||
if let Some(last_part) = cursor_parts.last() {
|
||||
if let Ok(cursor_value) = last_part.parse::<i64>() {
|
||||
// 使用游标值过滤,获取小于该值的记录(因为是降序排列)
|
||||
query = query.filter(im_room_member::Column::LastOptTime.lt(cursor_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let members = query.all(db).await.with_context(|| "查询房间成员失败")?;
|
||||
|
||||
// 构建下一页游标和判断是否为最后一页
|
||||
let (next_cursor, is_last) = if members.len() < cursor_page_param.page_size as usize {
|
||||
// 如果返回的记录数少于请求的页面大小,说明是最后一页
|
||||
(String::new(), true)
|
||||
} else if let Some(last_member) = members.last() {
|
||||
// 使用最后一条记录的 last_opt_time 构建下一页游标
|
||||
let next_cursor = format!("{}", last_member.last_opt_time);
|
||||
(next_cursor, false)
|
||||
} else {
|
||||
(String::new(), true)
|
||||
};
|
||||
|
||||
Ok(CursorPageResp {
|
||||
cursor: next_cursor,
|
||||
is_last,
|
||||
list: Some(members),
|
||||
total,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_room_page(
|
||||
page_param: PageParam,
|
||||
db: &DatabaseConnection,
|
||||
login_uid: &str,
|
||||
) -> Result<Page<im_room::Model>, CommonError> {
|
||||
// 计算偏移量
|
||||
let offset = (page_param.current - 1) * page_param.size;
|
||||
|
||||
// 查询总数
|
||||
let total = im_room::Entity::find()
|
||||
.filter(im_room::Column::LoginUid.eq(login_uid))
|
||||
.count(db)
|
||||
.await
|
||||
.with_context(|| "查询房间成员总数失败")?;
|
||||
|
||||
// 分页查询数据
|
||||
let records = im_room::Entity::find()
|
||||
.filter(im_room::Column::LoginUid.eq(login_uid))
|
||||
.offset(offset as u64)
|
||||
.limit(page_param.size as u64)
|
||||
.all(db)
|
||||
.await
|
||||
.with_context(|| "分页查询房间成员失败")?;
|
||||
|
||||
Ok(Page {
|
||||
records,
|
||||
total: total.to_string(),
|
||||
size: page_param.size.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save_room_batch(
|
||||
db: &DatabaseConnection,
|
||||
room_members: Vec<im_room::Model>,
|
||||
login_uid: &str,
|
||||
) -> Result<(), CommonError> {
|
||||
// 使用事务确保批量操作的原子性
|
||||
let txn = db.begin().await?;
|
||||
|
||||
for mut member in room_members {
|
||||
// 设置 login_uid
|
||||
member.login_uid = login_uid.to_string();
|
||||
|
||||
// 检查记录是否已存在
|
||||
let existing = im_room::Entity::find()
|
||||
.filter(im_room::Column::Id.eq(member.id.clone()))
|
||||
.filter(im_room::Column::LoginUid.eq(member.login_uid.clone()))
|
||||
.one(&txn)
|
||||
.await?;
|
||||
|
||||
if existing.is_none() {
|
||||
// 如果记录不存在,执行插入
|
||||
let member_active = member.into_active_model();
|
||||
member_active.insert(&txn).await?;
|
||||
}
|
||||
// 如果记录已存在,跳过插入
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
txn.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_room_members_by_room_id(
|
||||
room_id: &str,
|
||||
db: &DatabaseConnection,
|
||||
login_uid: &str,
|
||||
) -> Result<Vec<im_room_member::Model>, CommonError> {
|
||||
let members = im_room_member::Entity::find()
|
||||
.filter(im_room_member::Column::RoomId.eq(room_id))
|
||||
.filter(im_room_member::Column::LoginUid.eq(login_uid))
|
||||
.all(db)
|
||||
.await
|
||||
.with_context(|| "查询房间成员失败")?;
|
||||
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
pub async fn save_room_member_batch(
|
||||
db: &DatabaseConnection,
|
||||
room_members: Vec<im_room_member::Model>,
|
||||
room_id: i64,
|
||||
login_uid: &str,
|
||||
) -> Result<(), CommonError> {
|
||||
// 使用事务确保操作的原子性
|
||||
let txn = db.begin().await?;
|
||||
|
||||
// 根据room_id和login_uid查询现有数据
|
||||
let existing_members = im_room_member::Entity::find()
|
||||
.filter(im_room_member::Column::RoomId.eq(room_id.to_string()))
|
||||
.filter(im_room_member::Column::LoginUid.eq(login_uid))
|
||||
.all(&txn)
|
||||
.await
|
||||
.with_context(|| "查询房间成员失败")?;
|
||||
|
||||
if !existing_members.is_empty() {
|
||||
// 如果有数据,则删除当前用户的现有数据
|
||||
im_room_member::Entity::delete_many()
|
||||
.filter(im_room_member::Column::RoomId.eq(room_id.to_string()))
|
||||
.filter(im_room_member::Column::LoginUid.eq(login_uid))
|
||||
.exec(&txn)
|
||||
.await
|
||||
.with_context(|| "删除房间成员失败")?;
|
||||
}
|
||||
|
||||
// 保存新的room_members数据(批量插入)
|
||||
if !room_members.is_empty() {
|
||||
let active_models: Vec<im_room_member::ActiveModel> = room_members
|
||||
.into_iter()
|
||||
.map(|member| {
|
||||
let mut member_active = member.into_active_model();
|
||||
member_active.login_uid = Set(login_uid.to_string());
|
||||
member_active.room_id = Set(Some(room_id.to_string()));
|
||||
member_active
|
||||
})
|
||||
.collect();
|
||||
|
||||
im_room_member::Entity::insert_many(active_models)
|
||||
.exec(&txn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
txn.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_my_room_info(
|
||||
db: &DatabaseConnection,
|
||||
my_name: &str,
|
||||
room_id: &str,
|
||||
uid: &str,
|
||||
login_uid: &str,
|
||||
) -> Result<(), CommonError> {
|
||||
// 根据 room_id、uid 和 login_uid 查找房间成员记录
|
||||
let member = im_room_member::Entity::find()
|
||||
.filter(im_room_member::Column::RoomId.eq(room_id))
|
||||
.filter(im_room_member::Column::Uid.eq(uid))
|
||||
.filter(im_room_member::Column::LoginUid.eq(login_uid))
|
||||
.one(db)
|
||||
.await
|
||||
.with_context(|| "查询房间成员失败")?;
|
||||
|
||||
if let Some(member) = member {
|
||||
debug!("找到房间成员记录: {:?}", member);
|
||||
// 如果找到记录,更新 nickname 字段
|
||||
let mut member_active = member.into_active_model();
|
||||
member_active.my_name = Set(Some(my_name.to_string()));
|
||||
|
||||
member_active
|
||||
.update(db)
|
||||
.await
|
||||
.with_context(|| "更新房间成员昵称失败")?;
|
||||
info!("更新成员房间成员信息成功");
|
||||
Ok(())
|
||||
} else {
|
||||
// 如果没有找到记录,返回错误
|
||||
Err(CommonError::UnexpectedError(anyhow::anyhow!(
|
||||
"未找到指定的房间成员记录: room_id={}, uid={}",
|
||||
room_id,
|
||||
uid
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use crate::error::CommonError;
|
||||
use entity::im_user;
|
||||
use entity::prelude::ImUserEntity;
|
||||
use log::{error, info};
|
||||
use sea_orm::{ActiveValue::Set, ConnectionTrait, EntityTrait};
|
||||
|
||||
/// 更新用户的 is_init 状态
|
||||
pub async fn update_user_init_status<C>(
|
||||
db: &C,
|
||||
login_uid: &str,
|
||||
is_init: bool,
|
||||
) -> Result<(), CommonError>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
let user_update = im_user::ActiveModel {
|
||||
id: Set(login_uid.to_string()),
|
||||
is_init: Set(is_init),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match ImUserEntity::update(user_update).exec(db).await {
|
||||
Ok(_) => {
|
||||
info!("用户 {} 的 is_init 状态已更新为 {}", login_uid, is_init);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("更新用户 is_init 状态失败: {:?}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod im_config_repository;
|
||||
pub mod im_contact_repository;
|
||||
pub mod im_message_repository;
|
||||
pub mod im_room_member_repository;
|
||||
pub mod im_user_repository;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod user_info;
|
||||
pub mod vo;
|
||||
@@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct UserInfoVO {
|
||||
pub uid: String,
|
||||
pub account: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
pub avatar: String,
|
||||
pub sex: Option<i32>,
|
||||
pub user_state_id: Option<String>,
|
||||
pub modify_name_chance: Option<u32>,
|
||||
pub avatar_update_time: Option<i64>,
|
||||
pub context: bool,
|
||||
pub num: i32,
|
||||
pub update_time: Option<i64>,
|
||||
pub create_time: Option<i64>,
|
||||
pub token: String,
|
||||
pub client: String,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MyRoomInfoReq {
|
||||
pub id: String,
|
||||
// 我的群昵称
|
||||
pub my_name: String,
|
||||
// 群备注
|
||||
pub remark: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChatMessageReq {
|
||||
pub id: String,
|
||||
pub room_id: Option<String>,
|
||||
pub msg_type: Option<u8>,
|
||||
pub body: Option<serde_json::Value>,
|
||||
pub skip: Option<bool>,
|
||||
pub is_temp: Option<bool>,
|
||||
pub is_push_message: Option<bool>,
|
||||
}
|
||||
+18
-2
@@ -18,14 +18,15 @@ import { useStorage } from '@vueuse/core'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
const appWindow = WebviewWindow.getCurrent()
|
||||
const { createWebviewWindow } = useWindow()
|
||||
const settingStore = useSettingStore()
|
||||
const { themes, lockScreen, page } = storeToRefs(settingStore)
|
||||
const { resetLoginState, logout } = useLogin()
|
||||
const token = useStorage('TOKEN', null)
|
||||
const refreshToken = useStorage('REFRESH_TOKEN', null)
|
||||
const token = useStorage<string | null>('TOKEN', null)
|
||||
const refreshToken = useStorage<string | null>('REFRESH_TOKEN', null)
|
||||
|
||||
/** 不需要锁屏的页面 */
|
||||
const LockExclusion = new Set(['/login', '/tray', '/qrCode', '/about', '/onlineStatus'])
|
||||
@@ -108,6 +109,21 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
listen('refresh_token_event', (event) => {
|
||||
console.log('🔄 收到 refresh_token 事件')
|
||||
|
||||
// 从 event.payload 中获取 token 和 refreshToken
|
||||
const payload: any = event.payload
|
||||
|
||||
if (payload.token) {
|
||||
token.value = payload.token
|
||||
}
|
||||
|
||||
if (payload.refreshToken) {
|
||||
refreshToken.value = payload.refreshToken
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// 判断是否是桌面端,桌面端需要调整样式
|
||||
isDesktop.value && (await import('@/styles/scss/global/desktop.scss'))
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { writeImage } from '@tauri-apps/plugin-clipboard-manager'
|
||||
import { invokeWithErrorHandler, ErrorType } from '@/utils/TauriInvokeHandler.ts'
|
||||
import { useCanvasTool } from '@/hooks/useCanvasTool'
|
||||
import { useTauriListener } from '@/hooks/useTauriListener'
|
||||
|
||||
@@ -112,7 +112,10 @@ async function initCanvas() {
|
||||
height: `${canvasHeight}`
|
||||
}
|
||||
|
||||
const screenshotData = await invoke('screenshot', config)
|
||||
const screenshotData = await invokeWithErrorHandler('screenshot', config, {
|
||||
errorMessage: '截图失败',
|
||||
errorType: ErrorType.Client
|
||||
})
|
||||
|
||||
if (imgCanvas.value && maskCanvas.value) {
|
||||
imgCanvas.value.width = canvasWidth
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { RoomTypeEnum } from '@/enums'
|
||||
import { RoomTypeEnum, TauriCommand } from '@/enums'
|
||||
import { useBadgeInfo, useUserInfo } from '@/hooks/useCached.ts'
|
||||
import { useCommon } from '@/hooks/useCommon.ts'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
@@ -203,6 +203,9 @@ import apis from '@/services/apis.ts'
|
||||
import { useWindow } from '@/hooks/useWindow'
|
||||
import { useImageViewer } from '@/stores/imageViewer'
|
||||
import type { UserItem } from '@/services/types'
|
||||
import { useCachedStore } from '~/src/stores/cached'
|
||||
import { invokeWithErrorHandler } from '@/utils/TauriInvokeHandler'
|
||||
import { ErrorType } from '@/common/exception'
|
||||
|
||||
const { openMsgSession } = useCommon()
|
||||
const { createWebviewWindow } = useWindow()
|
||||
@@ -223,6 +226,7 @@ const remarkInputRef = useTemplateRef('remarkInputRef')
|
||||
const isEditingNickname = ref(false)
|
||||
const nicknameValue = ref('')
|
||||
const nicknameInputRef = useTemplateRef('nicknameInputRef')
|
||||
const cacheStore = useCachedStore()
|
||||
|
||||
watchEffect(() => {
|
||||
if (content.type === RoomTypeEnum.SINGLE) {
|
||||
@@ -255,7 +259,7 @@ const startEditRemark = () => {
|
||||
// 处理群备注更新
|
||||
const handleRemarkUpdate = async () => {
|
||||
if (remarkValue.value !== item.value.remark) {
|
||||
await apis.updateMyRoomInfo({
|
||||
await cacheStore.updateMyRoomInfo({
|
||||
id: item.value.roomId,
|
||||
remark: remarkValue.value,
|
||||
myName: item.value.myName || ''
|
||||
@@ -278,10 +282,10 @@ const startEditNickname = () => {
|
||||
// 处理本群昵称更新
|
||||
const handleNicknameUpdate = async () => {
|
||||
if (nicknameValue.value !== item.value.myName) {
|
||||
await apis.updateMyRoomInfo({
|
||||
await cacheStore.updateMyRoomInfo({
|
||||
id: item.value.roomId,
|
||||
myName: nicknameValue.value,
|
||||
remark: item.value.remark || ''
|
||||
remark: remarkValue.value,
|
||||
myName: item.value.myName || ''
|
||||
})
|
||||
item.value.myName = nicknameValue.value
|
||||
window.$message.success('本群昵称更新成功')
|
||||
@@ -300,12 +304,16 @@ const handleCopy = (account: string) => {
|
||||
// 获取群组详情和成员信息
|
||||
const fetchGroupMembers = async (roomId: string) => {
|
||||
try {
|
||||
const params = {
|
||||
roomId: roomId,
|
||||
current: 1,
|
||||
size: 10 // 获取前10个成员
|
||||
}
|
||||
const response = await apis.getGroupList(params)
|
||||
const response: any = await invokeWithErrorHandler(
|
||||
TauriCommand.GET_ROOM_MEMBERS,
|
||||
{
|
||||
roomId: roomId
|
||||
},
|
||||
{
|
||||
customErrorMessage: '获取群成员失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
)
|
||||
if (response && response.list) {
|
||||
// 使用每个成员的uid获取详细信息
|
||||
const memberDetails = response.list.map((member: UserItem) => {
|
||||
|
||||
@@ -139,8 +139,8 @@ import { getFilesMeta, getUserAbsoluteVideosDir } from '@/utils/PathUtil'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { join } from '@tauri-apps/api/path'
|
||||
|
||||
const { id } = defineProps<{
|
||||
id: SessionItem['id']
|
||||
const { detailId } = defineProps<{
|
||||
detailId: SessionItem['detailId']
|
||||
}>()
|
||||
const globalStore = useGlobalStore()
|
||||
const contactStore = useContactStore()
|
||||
@@ -175,7 +175,7 @@ const isSingleChat = computed(() => {
|
||||
/** 是否是好友关系 */
|
||||
const isFriend = computed(() => {
|
||||
if (!isSingleChat.value) return true
|
||||
return contactStore.contactsList.some((contact: ContactItem) => contact.uid === id)
|
||||
return contactStore.contactsList.some((contact: ContactItem) => contact.uid === detailId)
|
||||
})
|
||||
|
||||
// 监听emojiShow的变化,当emojiShow为true时关闭recentlyTip
|
||||
|
||||
@@ -386,6 +386,7 @@ import AvatarCropper from '@/components/common/AvatarCropper.vue'
|
||||
import { useAvatarUpload } from '@/hooks/useAvatarUpload'
|
||||
import { useWindow } from '@/hooks/useWindow'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useCachedStore } from '~/src/stores/cached'
|
||||
|
||||
const appWindow = WebviewWindow.getCurrent()
|
||||
const { activeItem } = defineProps<{
|
||||
@@ -408,6 +409,7 @@ const modalShow = ref(false)
|
||||
const sidebarShow = ref(false)
|
||||
const showLoading = ref(true)
|
||||
const isLoading = ref(false)
|
||||
const cacheStore = useCachedStore()
|
||||
// 群组详情数据
|
||||
const groupDetail = ref({
|
||||
myNickname: '', // 我在本群的昵称
|
||||
@@ -471,14 +473,14 @@ const MIN_LOADING_TIME = 300 // 最小加载时间(毫秒)
|
||||
const isOnline = computed(() => {
|
||||
if (activeItem.type === RoomTypeEnum.GROUP) return false
|
||||
|
||||
const contact = contactStore.contactsList.find((item) => item.uid === activeItem.id)
|
||||
const contact = contactStore.contactsList.find((item) => item.uid === activeItem.detailId)
|
||||
|
||||
return contact?.activeStatus === OnlineEnum.ONLINE
|
||||
})
|
||||
/** 是否还是好友 */
|
||||
const shouldShowDeleteFriend = computed(() => {
|
||||
if (activeItem.type === RoomTypeEnum.GROUP) return false
|
||||
return contactStore.contactsList.some((item) => item.uid === activeItem.id)
|
||||
return contactStore.contactsList.some((item) => item.uid === activeItem.detailId)
|
||||
})
|
||||
const groupUserList = computed(() => groupStore.userList)
|
||||
const messageOptions = computed(() => chatStore.currentMessageOptions)
|
||||
@@ -504,8 +506,8 @@ const currentUserStatus = computed(() => {
|
||||
if (activeItem.type === RoomTypeEnum.GROUP) return null
|
||||
|
||||
// 使用 useUserInfo 获取用户信息
|
||||
if (!activeItem.id) return null
|
||||
const userInfo = useUserInfo(activeItem.id).value
|
||||
if (!activeItem.detailId) return null
|
||||
const userInfo = useUserInfo(activeItem.detailId).value
|
||||
|
||||
// 从状态列表中找到对应的状态
|
||||
return userStatusStore.stateList.find((state: { id: string }) => state.id === userInfo.userStateId)
|
||||
@@ -526,8 +528,8 @@ const statusTitle = computed(() => {
|
||||
const currentUserAvatar = computed(() => {
|
||||
if (activeItem.type === RoomTypeEnum.GROUP) {
|
||||
return AvatarUtils.getAvatarUrl(activeItem.avatar)
|
||||
} else if (activeItem.id) {
|
||||
return AvatarUtils.getAvatarUrl(useUserInfo(activeItem.id).value.avatar || activeItem.avatar)
|
||||
} else if (activeItem.detailId) {
|
||||
return AvatarUtils.getAvatarUrl(useUserInfo(activeItem.detailId).value.avatar || activeItem.avatar)
|
||||
}
|
||||
return AvatarUtils.getAvatarUrl(activeItem.avatar)
|
||||
})
|
||||
@@ -632,7 +634,7 @@ const handleCreateGroupOrInvite = () => {
|
||||
|
||||
/** 处理创建群聊 */
|
||||
const handleCreateGroup = () => {
|
||||
useMitt.emit(MittEnum.CREATE_GROUP, activeItem.id)
|
||||
useMitt.emit(MittEnum.CREATE_GROUP, activeItem.detailId)
|
||||
}
|
||||
|
||||
/** 处理邀请进群 */
|
||||
@@ -684,7 +686,7 @@ const saveGroupInfo = async () => {
|
||||
// 只有当数据发生变化时才发送请求
|
||||
if (nicknameChanged || remarkChanged) {
|
||||
// 使用updateMyRoomInfo接口更新我在群里的昵称和群备注
|
||||
await apis.updateMyRoomInfo({
|
||||
await cacheStore.updateMyRoomInfo({
|
||||
id: activeItem.roomId,
|
||||
myName: groupDetail.value.myNickname,
|
||||
remark: groupDetail.value.groupRemark
|
||||
@@ -852,8 +854,8 @@ const handleDelete = (label: RoomActEnum) => {
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (optionsType.value === RoomActEnum.DELETE_FRIEND && activeItem.id) {
|
||||
contactStore.onDeleteContact(activeItem.id).then(() => {
|
||||
if (optionsType.value === RoomActEnum.DELETE_FRIEND && activeItem.detailId) {
|
||||
contactStore.onDeleteContact(activeItem.detailId).then(() => {
|
||||
modalShow.value = false
|
||||
sidebarShow.value = false
|
||||
window.$message.success('已删除好友')
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
item-resizable
|
||||
@scroll="handleScroll($event)"
|
||||
:item-size="46"
|
||||
:items="filteredUserList">
|
||||
:items="groupStore.userList">
|
||||
<template #default="{ item }">
|
||||
<n-popover
|
||||
:ref="(el) => (infoPopoverRefs[item.uid] = el)"
|
||||
@@ -146,7 +146,9 @@
|
||||
@error="userLoadedMap[item.uid] = true" />
|
||||
</div>
|
||||
<n-flex vertical :size="2" class="flex-1 truncate">
|
||||
<p :title="item.name" class="text-12px truncate flex-1">{{ item.name }}</p>
|
||||
<p :title="item.name" class="text-12px truncate flex-1">
|
||||
{{ item.myName ? item.myName : item.name }}
|
||||
</p>
|
||||
<n-flex
|
||||
v-if="item.userStateId && getUserState(item.userStateId)"
|
||||
align="center"
|
||||
@@ -161,12 +163,12 @@
|
||||
</n-flex>
|
||||
|
||||
<div
|
||||
v-if="item.roleId === RoleEnum.LORD"
|
||||
v-if="item.groupRole === RoleEnum.LORD"
|
||||
class="flex px-4px bg-#d5304f30 py-3px rounded-4px size-fit select-none">
|
||||
<p class="text-(10px #d5304f)">群主</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.roleId === RoleEnum.ADMIN"
|
||||
v-if="item.groupRole === RoleEnum.ADMIN"
|
||||
class="flex px-4px bg-#1a7d6b30 py-3px rounded-4px size-fit select-none">
|
||||
<p class="text-(10px #008080)">管理员</p>
|
||||
</div>
|
||||
@@ -189,7 +191,6 @@ import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import { useGroupStore } from '@/stores/group.ts'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import type { UserItem } from '@/services/types.ts'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { useCachedStore } from '@/stores/cached.ts'
|
||||
@@ -216,42 +217,6 @@ const isLoadingMembers = ref(true)
|
||||
const isLoadingAnnouncement = ref(true)
|
||||
// 在线人数加载状态
|
||||
const isLoadingOnlineCount = ref(true)
|
||||
const groupUserList = computed(() => groupStore.userList)
|
||||
const userList = computed(() => {
|
||||
// 先获取所有需要的用户ID
|
||||
const userIds = groupUserList.value.map((item) => item.uid)
|
||||
|
||||
// 确保所有用户信息都被加载
|
||||
if (userIds.length > 0) {
|
||||
cachedStore.getBatchUserInfo(userIds)
|
||||
}
|
||||
|
||||
return groupUserList.value
|
||||
.map((item: UserItem) => {
|
||||
const cachedUser = useUserInfo(item.uid).value
|
||||
// 合并数据时保留所有需要的字段
|
||||
return {
|
||||
...item, // 保留原始数据
|
||||
...cachedUser, // 合并缓存的用户数据
|
||||
account: cachedUser.account || item.account, // 确保accountCode被保留
|
||||
uid: item.uid // 确保uid被保留
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// roleId === 1 的排在最前面
|
||||
// 首先按照roleId排序
|
||||
if (a.roleId !== b.roleId) {
|
||||
// roleId === 2 的排在第二位
|
||||
if (a.roleId === RoleEnum.LORD) return -1
|
||||
if (b.roleId === RoleEnum.LORD) return 1
|
||||
if (a.roleId === RoleEnum.ADMIN) return -1
|
||||
if (b.roleId === RoleEnum.ADMIN) return 1
|
||||
}
|
||||
// roleId相同时,按照activeStatus升序排序
|
||||
return a.activeStatus - b.activeStatus
|
||||
})
|
||||
})
|
||||
const filteredUserList = shallowRef(userList.value)
|
||||
const isGroup = computed(() => globalStore.currentSession?.type === RoomTypeEnum.GROUP)
|
||||
/** 是否是搜索模式 */
|
||||
const isSearch = ref(false)
|
||||
@@ -265,11 +230,11 @@ const { handlePopoverUpdate, enableScroll } = usePopover(selectKey, 'image-chat-
|
||||
provide('popoverControls', { enableScroll })
|
||||
|
||||
const isLord = computed(() => {
|
||||
const currentUser = groupUserList.value.find((user) => user.uid === useUserStore().userInfo?.uid)
|
||||
const currentUser = groupStore.userList.find((user) => user.uid === useUserStore().userInfo?.uid)
|
||||
return currentUser?.roleId === RoleEnum.LORD
|
||||
})
|
||||
const isAdmin = computed(() => {
|
||||
const currentUser = groupUserList.value.find((user) => user.uid === useUserStore().userInfo?.uid)
|
||||
const currentUser = groupStore.userList.find((user) => user.uid === useUserStore().userInfo?.uid)
|
||||
return currentUser?.roleId === RoleEnum.ADMIN
|
||||
})
|
||||
|
||||
@@ -296,7 +261,7 @@ const mergedUserList = computed(() => {
|
||||
const userMap = new Map()
|
||||
|
||||
// 首先添加在线用户列表
|
||||
userList.value.forEach((user) => {
|
||||
groupStore.userList.forEach((user) => {
|
||||
userMap.set(user.uid, user)
|
||||
})
|
||||
|
||||
@@ -318,19 +283,17 @@ const mergedUserList = computed(() => {
|
||||
|
||||
// 修改watch监听器
|
||||
watch(
|
||||
[userList, () => cachedStore.currentAtUsersList],
|
||||
[() => groupStore.userList, () => cachedStore.currentAtUsersList],
|
||||
() => {
|
||||
// 如果正在搜索,则应用搜索过滤
|
||||
if (searchRef.value) {
|
||||
filteredUserList.value = mergedUserList.value.filter((user) =>
|
||||
groupStore.userList = mergedUserList.value.filter((user) =>
|
||||
user.name.toLowerCase().includes(searchRef.value.toLowerCase())
|
||||
)
|
||||
} else {
|
||||
filteredUserList.value = userList.value
|
||||
}
|
||||
|
||||
// 判断成员列表是否已加载完成
|
||||
if (userList.value.length > 0 && currentLoadingRoomId.value === globalStore.currentSession?.roomId) {
|
||||
if (groupStore.userList.length > 0 && currentLoadingRoomId.value === globalStore.currentSession?.roomId) {
|
||||
isLoadingMembers.value = false
|
||||
}
|
||||
},
|
||||
@@ -342,14 +305,8 @@ watch(
|
||||
* @param value 输入值
|
||||
*/
|
||||
const handleSearch = useDebounceFn((value: string) => {
|
||||
if (!value) {
|
||||
// 如果搜索框为空,只显示在线用户列表
|
||||
filteredUserList.value = userList.value
|
||||
return
|
||||
}
|
||||
|
||||
// 从合并后的用户列表中搜索
|
||||
filteredUserList.value = mergedUserList.value.filter((user) => user.name.toLowerCase().includes(value.toLowerCase()))
|
||||
groupStore.userList = mergedUserList.value.filter((user) => user.name.toLowerCase().includes(value.toLowerCase()))
|
||||
}, 10)
|
||||
|
||||
/**
|
||||
@@ -359,8 +316,6 @@ const handleBlur = () => {
|
||||
if (searchRef.value) return
|
||||
isSearch.value = false
|
||||
searchRef.value = ''
|
||||
// 重置为只显示在线用户列表
|
||||
filteredUserList.value = userList.value
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -481,7 +436,7 @@ onMounted(async () => {
|
||||
currentLoadingRoomId.value = newSession.roomId
|
||||
// 重置群组数据后再加载新的群成员数据
|
||||
groupStore.resetGroupData()
|
||||
await groupStore.getGroupUserList(true, newSession.roomId)
|
||||
await groupStore.getGroupUserList()
|
||||
// 获取群组统计信息(包括在线人数)
|
||||
await groupStore.getCountStatistic()
|
||||
isLoadingOnlineCount.value = false
|
||||
@@ -494,8 +449,8 @@ onMounted(async () => {
|
||||
)
|
||||
|
||||
// 初始化时获取当前群组用户的信息
|
||||
if (groupUserList.value.length > 0) {
|
||||
await cachedStore.getBatchUserInfo(groupUserList.value.map((item) => item.uid))
|
||||
if (groupStore.userList.length > 0) {
|
||||
await cachedStore.getBatchUserInfo(groupStore.userList.map((item) => item.uid))
|
||||
const handleAnnounInitOnEvent = (shouldReload: boolean) => {
|
||||
return async (event: any) => {
|
||||
if (shouldReload || event) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- 中间聊天框内容 -->
|
||||
<ChatMain :active-item="activeItemRef" />
|
||||
<!-- 输入框和操作列表 -->
|
||||
<ChatFooter class="flex-1" :id="activeItemRef.id" />
|
||||
<ChatFooter class="flex-1" :detail-id="activeItemRef.detailId" />
|
||||
</n-flex>
|
||||
<ChatSidebar />
|
||||
</n-flex>
|
||||
@@ -16,6 +16,9 @@ import type { SessionItem } from '@/services/types.ts'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useTauriListener } from '@/hooks/useTauriListener'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { useChatStore } from '~/src/stores/chat'
|
||||
import { MessageStatusEnum } from '~/src/enums'
|
||||
|
||||
const { addListener } = useTauriListener()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -24,6 +27,7 @@ const appWindow = WebviewWindow.getCurrent()
|
||||
const { activeItem } = defineProps<{
|
||||
activeItem?: SessionItem
|
||||
}>()
|
||||
const chatStore = useChatStore()
|
||||
provide('activeItem', { ...activeItem! })
|
||||
const activeItemRef = ref({ ...activeItem! })
|
||||
|
||||
@@ -31,11 +35,36 @@ watchEffect(() => {
|
||||
activeItemRef.value = { ...activeItem! }
|
||||
})
|
||||
|
||||
// 注册 Tauri 事件监听器
|
||||
const unlistenSuccess = await listen('send_msg_success', async (event) => {
|
||||
let msg = event.payload as any
|
||||
chatStore.updateMsg({
|
||||
msgId: msg.oldMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: msg.message.id,
|
||||
body: msg.message.body
|
||||
})
|
||||
})
|
||||
|
||||
const unlistenError = await listen('send_msg_error', (event) => {
|
||||
let msgId = event.payload as any
|
||||
chatStore.updateMsg({
|
||||
msgId: msgId,
|
||||
status: MessageStatusEnum.FAILED
|
||||
})
|
||||
})
|
||||
|
||||
addListener(
|
||||
appWindow.listen(appWindow.label, (e: { payload: SessionItem }) => {
|
||||
activeItemRef.value = e.payload
|
||||
})
|
||||
)
|
||||
|
||||
// 在组件卸载时清理监听器
|
||||
onUnmounted(() => {
|
||||
unlistenSuccess()
|
||||
unlistenError()
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
/**! 修改naive-ui虚拟列表滚动条的间距 */
|
||||
|
||||
@@ -455,3 +455,27 @@ export enum NotificationTypeEnum {
|
||||
/** 接收但不提醒[免打扰] */
|
||||
NOT_DISTURB = 1
|
||||
}
|
||||
|
||||
/** Tauri 命令 */
|
||||
export enum TauriCommand {
|
||||
/** 更新我的群聊信息 */
|
||||
UPDATE_MY_ROOM_INFO = 'update_my_room_info',
|
||||
/** 获取房间成员 */
|
||||
GET_ROOM_MEMBERS = 'get_room_members',
|
||||
/** 分页查询所有房间 */
|
||||
PAGE_ROOM = 'page_room',
|
||||
/** 分页查询房间成员 */
|
||||
CURSOR_PAGE_ROOM_MEMBERS = 'cursor_page_room_members',
|
||||
/** 列出所有会话列表 */
|
||||
LIST_CONTACTS = 'list_contacts_command',
|
||||
/** 分页查询会话消息 */
|
||||
PAGE_MSG = 'page_msg',
|
||||
/** 保存用户信息 */
|
||||
SAVE_USER_INFO = 'save_user_info',
|
||||
/** 更新用户最后操作时间 */
|
||||
UPDATE_USER_LAST_OPT_TIME = 'update_user_last_opt_time',
|
||||
/** 发送消息 */
|
||||
SEND_MSG = 'send_msg',
|
||||
/** 保存消息 */
|
||||
SAVE_MSG = 'save_msg'
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { EventEnum, RoomTypeEnum } from '@/enums'
|
||||
import { EventEnum, RoomTypeEnum, TauriCommand } from '@/enums'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invokeSilently } from '@/utils/TauriInvokeHandler.ts'
|
||||
import { LoginStatus, useWsLoginStore } from '@/stores/ws'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
@@ -44,6 +44,7 @@ export const useLogin = () => {
|
||||
await resizeWindow('tray', 130, 44)
|
||||
// 发送登出事件
|
||||
await emit(EventEnum.LOGOUT)
|
||||
await invokeSilently(TauriCommand.UPDATE_USER_LAST_OPT_TIME)
|
||||
} catch (error) {
|
||||
console.error('创建登录窗口失败:', error)
|
||||
}
|
||||
@@ -71,7 +72,7 @@ export const useLogin = () => {
|
||||
chatStore.clearUnreadCount()
|
||||
// 5. 清除系统托盘图标上的未读数
|
||||
try {
|
||||
await invoke('set_badge_count', { count: null })
|
||||
await invokeSilently('set_badge_count', { count: null })
|
||||
} catch (error) {
|
||||
console.error('清除系统托盘图标上的未读数失败:', error)
|
||||
}
|
||||
|
||||
+89
-72
@@ -1,6 +1,5 @@
|
||||
import { LimitEnum, MittEnum, MsgEnum, MessageStatusEnum, RoomTypeEnum, UploadSceneEnum } from '@/enums'
|
||||
import { LimitEnum, MittEnum, MsgEnum, MessageStatusEnum, RoomTypeEnum, UploadSceneEnum, TauriCommand } from '@/enums'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
import apis from '@/services/apis.ts'
|
||||
import { useCachedStore, type BaseUserItem } from '@/stores/cached.ts'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
@@ -19,6 +18,8 @@ import type { AIModel } from '@/services/types.ts'
|
||||
import { UploadProviderEnum, useUpload } from './useUpload.ts'
|
||||
import { getReplyContent } from '@/utils/MessageReply.ts'
|
||||
import { fixFileMimeType, getMessageTypeByFile } from '@/utils/FileType.ts'
|
||||
import { invokeWithErrorHandler } from '@/utils/TauriInvokeHandler'
|
||||
import { ErrorType } from '@/common/exception'
|
||||
/**
|
||||
* 光标管理器
|
||||
*/
|
||||
@@ -464,22 +465,31 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
console.log('视频上传完成,更新为服务器URL:', messageBody.url)
|
||||
}
|
||||
// 发送消息到服务器
|
||||
const res = await apis.sendMsg({
|
||||
roomId: globalStore.currentSession.roomId,
|
||||
msgType: msg.type,
|
||||
body: messageBody
|
||||
})
|
||||
await invokeWithErrorHandler(
|
||||
TauriCommand.SEND_MSG,
|
||||
{
|
||||
data: {
|
||||
id: tempMsgId,
|
||||
roomId: globalStore.currentSession.roomId,
|
||||
msgType: msg.type,
|
||||
body: messageBody
|
||||
}
|
||||
},
|
||||
{
|
||||
customErrorMessage: '消息发送失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
)
|
||||
|
||||
// 停止发送状态的定时器
|
||||
clearTimeout(statusTimer)
|
||||
|
||||
// 更新消息状态为成功,并使用服务器返回的消息体
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: res.message.id,
|
||||
body: res.message.body
|
||||
})
|
||||
// chatStore.updateMsg({
|
||||
// msgId: tempMsgId,
|
||||
// status: MessageStatusEnum.SUCCESS,
|
||||
// newMsgId: res,
|
||||
// })
|
||||
|
||||
// 更新会话最后活动时间
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
@@ -934,31 +944,31 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
thumbnailUploadResponse?.downloadUrl || `${qiniuConfig.domain}/${thumbnailUploadResponse?.key}`
|
||||
|
||||
// 发送消息到服务器保存
|
||||
const serverResponse = await apis.sendMsg({
|
||||
roomId: globalStore.currentSession.roomId,
|
||||
msgType: MsgEnum.VIDEO,
|
||||
body: {
|
||||
url: finalVideoUrl,
|
||||
size: processedFile.size,
|
||||
fileName: processedFile.name,
|
||||
thumbUrl: finalThumbnailUrl,
|
||||
thumbWidth: 300,
|
||||
thumbHeight: 150,
|
||||
thumbSize: thumbnailFile.size,
|
||||
localPath: videoPath, // 保存本地缓存路径
|
||||
senderUid: userUid.value // 保存发送者UID
|
||||
await invokeWithErrorHandler(
|
||||
TauriCommand.SEND_MSG,
|
||||
{
|
||||
data: {
|
||||
id: tempMsgId,
|
||||
roomId: globalStore.currentSession.roomId,
|
||||
msgType: MsgEnum.VIDEO,
|
||||
body: {
|
||||
url: finalVideoUrl,
|
||||
size: processedFile.size,
|
||||
fileName: processedFile.name,
|
||||
thumbUrl: finalThumbnailUrl,
|
||||
thumbWidth: 300,
|
||||
thumbHeight: 150,
|
||||
thumbSize: thumbnailFile.size,
|
||||
localPath: videoPath, // 保存本地缓存路径
|
||||
senderUid: userUid.value // 保存发送者UID
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
customErrorMessage: '视频消息发送失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
})
|
||||
|
||||
// 使用服务器返回的数据更新消息状态为SUCCESS,清除进度信息
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: serverResponse.message.id, // 使用服务器返回的消息ID
|
||||
body: serverResponse.message.body, // 使用服务器返回的消息体
|
||||
uploadProgress: undefined // 清除进度信息
|
||||
})
|
||||
|
||||
)
|
||||
// 清理本地URL
|
||||
URL.revokeObjectURL(tempMsg.message.body.url)
|
||||
URL.revokeObjectURL(localThumbUrl)
|
||||
@@ -1002,19 +1012,21 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
console.log('🖼️ 图片上传完成,更新为服务器URL:', messageBody.url)
|
||||
|
||||
// 发送消息到服务器
|
||||
const serverResponse = await apis.sendMsg({
|
||||
roomId: globalStore.currentSession.roomId,
|
||||
msgType: MsgEnum.IMAGE,
|
||||
body: messageBody
|
||||
})
|
||||
|
||||
// 更新消息状态为成功,并使用服务器返回的消息体
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: serverResponse.message.id,
|
||||
body: serverResponse.message.body
|
||||
})
|
||||
await invokeWithErrorHandler(
|
||||
TauriCommand.SEND_MSG,
|
||||
{
|
||||
data: {
|
||||
id: tempMsgId,
|
||||
roomId: globalStore.currentSession.roomId,
|
||||
msgType: MsgEnum.IMAGE,
|
||||
body: messageBody
|
||||
}
|
||||
},
|
||||
{
|
||||
customErrorMessage: '图片消息发送失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
)
|
||||
|
||||
// 更新会话最后活动时间
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
@@ -1094,24 +1106,26 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
console.log('📎 文件上传完成,更新为服务器URL:', messageBody.url)
|
||||
|
||||
// 发送消息到服务器
|
||||
const serverResponse = await apis.sendMsg({
|
||||
roomId: globalStore.currentSession.roomId,
|
||||
msgType: MsgEnum.FILE,
|
||||
body: messageBody
|
||||
})
|
||||
|
||||
// 更新消息状态为成功,并使用服务器返回的消息体
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: serverResponse.message.id,
|
||||
body: serverResponse.message.body
|
||||
})
|
||||
await invokeWithErrorHandler(
|
||||
TauriCommand.SEND_MSG,
|
||||
{
|
||||
data: {
|
||||
id: tempMsgId,
|
||||
roomId: globalStore.currentSession.roomId,
|
||||
msgType: MsgEnum.FILE,
|
||||
body: messageBody
|
||||
}
|
||||
},
|
||||
{
|
||||
customErrorMessage: '文件消息发送失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
)
|
||||
|
||||
// 更新会话最后活动时间
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
|
||||
console.log('📎 文件消息发送成功:', serverResponse.message.id)
|
||||
// console.log('📎 文件消息发送成功:', serverResponse.message.id)
|
||||
|
||||
// 清理进度监听器
|
||||
if (progressUnsubscribe) {
|
||||
@@ -1227,22 +1241,25 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
})
|
||||
|
||||
const sendData = {
|
||||
id: tempMsgId,
|
||||
roomId: globalStore.currentSession.roomId,
|
||||
msgType: MsgEnum.VOICE,
|
||||
body: messageBody
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await apis.sendMsg(sendData)
|
||||
await invokeWithErrorHandler(
|
||||
TauriCommand.SEND_MSG,
|
||||
{
|
||||
data: sendData
|
||||
},
|
||||
{
|
||||
customErrorMessage: '语音消息发送失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
)
|
||||
// 停止发送状态的定时器
|
||||
clearTimeout(statusTimer)
|
||||
// 更新消息状态为成功,并使用服务器返回的消息体
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: res.message.id,
|
||||
body: res.message.body
|
||||
})
|
||||
|
||||
// 更新会话最后活动时间
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
|
||||
@@ -149,7 +149,7 @@ export const useNetworkReconnect = () => {
|
||||
}
|
||||
// 如果当前是群聊,刷新群组信息
|
||||
if (globalStore.currentSession?.type === RoomTypeEnum.GROUP) {
|
||||
await groupStore.getGroupUserList(true)
|
||||
await groupStore.getGroupUserList()
|
||||
await groupStore.getCountStatistic()
|
||||
await cachedStore.getGroupAtUserBaseInfo()
|
||||
}
|
||||
|
||||
+14
-1
@@ -27,7 +27,15 @@
|
||||
<script setup lang="ts">
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import { ChangeTypeEnum, MittEnum, ModalEnum, NotificationTypeEnum, OnlineEnum, RoomTypeEnum } from '@/enums'
|
||||
import {
|
||||
ChangeTypeEnum,
|
||||
MittEnum,
|
||||
ModalEnum,
|
||||
NotificationTypeEnum,
|
||||
OnlineEnum,
|
||||
RoomTypeEnum,
|
||||
TauriCommand
|
||||
} from '@/enums'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useContactStore } from '@/stores/contacts.ts'
|
||||
@@ -48,6 +56,7 @@ import { useConfigStore } from '@/stores/config'
|
||||
import { useCheckUpdate } from '@/hooks/useCheckUpdate'
|
||||
import { UserAttentionType } from '@tauri-apps/api/window'
|
||||
import { LogicalSize } from '@tauri-apps/api/dpi'
|
||||
import { invokeSilently } from '@/utils/TauriInvokeHandler'
|
||||
|
||||
const loadingPercentage = ref(10)
|
||||
const loadingText = ref('正在加载应用...')
|
||||
@@ -233,6 +242,10 @@ useMitt.on(WsResponseMessageType.MY_ROOM_INFO_CHANGE, (data: { myName: string; r
|
||||
})
|
||||
useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => {
|
||||
chatStore.pushMsg(data)
|
||||
console.log('监听到接收消息', data)
|
||||
await invokeSilently(TauriCommand.SAVE_MSG, {
|
||||
data
|
||||
})
|
||||
const username = useUserInfo(data.fromUser.uid).value.name!
|
||||
const home = await WebviewWindow.getByLabel('home')
|
||||
// 当home窗口不显示并且home窗口不是最小化的时候并且不是聚焦窗口的时候
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ListResponse,
|
||||
LoginUserReq,
|
||||
MarkMsgReq,
|
||||
MessageReq,
|
||||
MessageType,
|
||||
MsgReadUnReadCountType,
|
||||
PageInfo,
|
||||
@@ -21,7 +20,6 @@ import {
|
||||
RequestFriendItem,
|
||||
SessionItem,
|
||||
UserInfoType,
|
||||
UserItem,
|
||||
UserState,
|
||||
Login,
|
||||
SearchFriend,
|
||||
@@ -40,12 +38,10 @@ const PUT = <T>(url: string, params?: any, abort?: AbortController) => request.p
|
||||
const DELETE = <T>(url: string, params?: any, abort?: AbortController) => request.delete<T>(url, params, abort)
|
||||
|
||||
export default {
|
||||
/** 获取群成员列表 */
|
||||
getGroupList: (params?: any) => GET<ListResponse<UserItem>>(urls.getGroupUserList, params),
|
||||
/** 获取群成员统计 */
|
||||
getMemberStatistic: () => GET<GroupStatisticType>(urls.getMemberStatistic),
|
||||
/** 房间内的所有群成员列表-@专用 */
|
||||
getAllUserBaseInfo: (params?: any) => GET<CacheUserItem[]>(urls.getAllUserBaseInfo, params),
|
||||
// getAllUserBaseInfo: (params?: any) => GET<CacheUserItem[]>(urls.getAllUserBaseInfo, params),
|
||||
/** 批量获取成员详细信息 */
|
||||
getUserInfoBatch: (users: CacheUserReq[]) => POST<CacheUserItem[]>(urls.getUserInfoBatch, { reqList: users }),
|
||||
/** 批量获取徽章信息 */
|
||||
@@ -53,7 +49,7 @@ export default {
|
||||
/** 获取消息列表 */
|
||||
getMsgList: (params?: any) => GET<ListResponse<MessageType>>(urls.getMsgList, params),
|
||||
/** 发送消息 */
|
||||
sendMsg: (data?: MessageReq) => POST<MessageType>(urls.sendMsg, data),
|
||||
// sendMsg: (data?: MessageReq) => POST<MessageType>(urls.sendMsg, data),
|
||||
/** 标记消息,点赞等 */
|
||||
markMsg: (data?: MarkMsgReq) => PUT<void>(urls.markMsg, data),
|
||||
/** 获取用户详细信息 */
|
||||
@@ -97,7 +93,7 @@ export default {
|
||||
/** 好友申请未读数 */
|
||||
newFriendCount: () => GET<{ unReadCount: number }>(urls.newFriendCount),
|
||||
/** 会话列表 */
|
||||
getSessionList: (params?: any) => GET<ListResponse<SessionItem>>(urls.getSessionList, params),
|
||||
// getSessionList: (params?: any) => GET<ListResponse<SessionItem>>(urls.getSessionList, params),
|
||||
/** 消息的已读未读列表 */
|
||||
getMsgReadList: (params?: any) => GET<ListResponse<{ uid: string }>>(urls.getMsgReadList, params),
|
||||
/** 消息已读未读数 */
|
||||
@@ -138,8 +134,8 @@ export default {
|
||||
/** 修改群信息(群主) */
|
||||
updateRoomInfo: (params: { id: string; name: string; avatar: string }) => POST<void>(urls.updateRoomInfo, params),
|
||||
/** 修改“我”的群聊名称 */
|
||||
updateMyRoomInfo: (params: { id: string; myName: string; remark: string }) =>
|
||||
POST<void>(urls.updateMyRoomInfo, params),
|
||||
// updateMyRoomInfo: (params: { id: string; myName: string; remark: string }) =>
|
||||
// POST<void>(urls.updateMyRoomInfo, params),
|
||||
/** 添加群管理 */
|
||||
addAdmin: ({ roomId, uidList }: { roomId: string; uidList: string[] }) =>
|
||||
PUT<boolean>(urls.addAdmin, {
|
||||
|
||||
@@ -516,6 +516,8 @@ export type SessionItem = {
|
||||
avatar: string
|
||||
/** 如果是单聊,则是对方的uid,如果是群聊,则是群id */
|
||||
id: string
|
||||
/** 如果是单聊,则是对方的uid,如果是群聊,则是群id */
|
||||
detailId: string
|
||||
/** 是否全员展示的会话 0否 1是 */
|
||||
hotFlag: IsAllUserEnum
|
||||
/** 会话名称 */
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
getBadgesBatch: `${prefix + URLEnum.USER}/badges/batch`,
|
||||
getAllUserBaseInfo: `${prefix + URLEnum.ROOM}/group/member/list`, // 房间内的所有群成员列表-@专用
|
||||
getMsgList: `${prefix + URLEnum.CHAT}/msg/page`,
|
||||
sendMsg: `${prefix + URLEnum.CHAT}/msg`,
|
||||
// sendMsg: `${prefix + URLEnum.CHAT}/msg`,
|
||||
getUserInfoDetail: `${prefix + URLEnum.USER}/userInfo`, // 获取用户信息详情
|
||||
modifyUserName: `${prefix + URLEnum.USER}/name`, // 修改用户名
|
||||
setUserBadge: `${prefix + URLEnum.USER}/badge`, // 设置用户徽章
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
modifyFriendRemark: `${prefix + URLEnum.USER}/friend/updateRemark`, // 修改好友备注
|
||||
|
||||
// -------------- 聊天室相关 ---------------
|
||||
getSessionList: `${prefix + URLEnum.CHAT}/contact/page`, // 会话列表
|
||||
// getSessionList: `${prefix + URLEnum.CHAT}/contact/page`, // 会话列表
|
||||
getMsgReadList: `${prefix + URLEnum.CHAT}/msg/read/page`, // 消息的已读未读列表
|
||||
getMsgReadCount: `${prefix + URLEnum.CHAT}/msg/read`, // 消息已读未读数
|
||||
sessionDetail: `${prefix + URLEnum.CHAT}/contact/detail`, // 会话详情
|
||||
@@ -63,7 +63,7 @@ export default {
|
||||
shield: `${prefix + URLEnum.CHAT}/setShield`, // 屏蔽消息
|
||||
// -------------- 群聊相关 ---------------
|
||||
createGroup: `${prefix + URLEnum.ROOM}/group`, // 新增群组
|
||||
getGroupUserList: `${prefix + URLEnum.ROOM}/group/member/page`, // 群成员列表
|
||||
// getGroupUserList: `${prefix + URLEnum.ROOM}/group/member/page`, // 群成员列表
|
||||
inviteGroupMember: `${prefix + URLEnum.ROOM}/group/member`, // 邀请群成员 和 移出群成员(post 和 delete)
|
||||
exitGroup: `${prefix + URLEnum.ROOM}/group/member/exit`, // 退群
|
||||
addAdmin: `${prefix + URLEnum.ROOM}/group/admin`, // 添加管理员
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
groupDetail: `${prefix + URLEnum.ROOM}/group`, // 群组详情
|
||||
groupList: `${prefix + URLEnum.ROOM}/group/list`, // 群聊列表
|
||||
updateRoomInfo: `${prefix + URLEnum.ROOM}/updateRoomInfo`, // 修改群信息(群主)
|
||||
updateMyRoomInfo: `${prefix + URLEnum.ROOM}/updateMyRoomInfo`, // 修改“我”的群聊名称
|
||||
// updateMyRoomInfo: `${prefix + URLEnum.ROOM}/updateMyRoomInfo`, // 修改“我”的群聊名称
|
||||
searchGroup: `${prefix + URLEnum.ROOM}/search`, // 搜索群聊
|
||||
applyGroup: `${prefix + URLEnum.ROOM}/applyGroup`, // 申请加群
|
||||
getAnnouncementList: `${prefix + URLEnum.ROOM}/announcement/list`, // 获取群公告
|
||||
|
||||
+39
-5
@@ -4,7 +4,9 @@ import { useGlobalStore } from '@/stores/global'
|
||||
import type { CacheBadgeItem, CacheUserItem } from '@/services/types'
|
||||
import { isDiffNow10Min } from '@/utils/ComputedTime.ts'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { StoresEnum } from '@/enums'
|
||||
import { StoresEnum, TauriCommand } from '@/enums'
|
||||
import { invokeWithErrorHandler, invokeSilently, ErrorType } from '@/utils/TauriInvokeHandler.ts'
|
||||
import { useGroupStore } from './group'
|
||||
|
||||
// 定义基础用户信息类型,只包含uid、头像和名称
|
||||
export type BaseUserItem = Pick<CacheUserItem, 'uid' | 'avatar' | 'name' | 'account'>
|
||||
@@ -157,7 +159,17 @@ export const useCachedStore = defineStore(StoresEnum.CACHED, () => {
|
||||
const initAllUserBaseInfo = async () => {
|
||||
// 这里获取的是全员群的全部用户信息,所以取1作为roomId
|
||||
if (localStorage.getItem('IS_INIT_USER_BASE') === null) {
|
||||
const data = await apis.getAllUserBaseInfo({ roomId: 1 })
|
||||
// const data = await apis.getAllUserBaseInfo({ roomId: 1 })
|
||||
const data: any = await invokeWithErrorHandler(
|
||||
TauriCommand.GET_ROOM_MEMBERS,
|
||||
{
|
||||
roomId: '1'
|
||||
},
|
||||
{
|
||||
customErrorMessage: '获取房间成员失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
)
|
||||
for (const item of data || []) {
|
||||
userCachedList[item.uid] = item
|
||||
}
|
||||
@@ -165,12 +177,26 @@ export const useCachedStore = defineStore(StoresEnum.CACHED, () => {
|
||||
}
|
||||
}
|
||||
|
||||
const groupStore = useGroupStore()
|
||||
|
||||
/** 获取群组内可@的用户基本信息
|
||||
* 如果是大厅(roomId=1)则不执行
|
||||
*/
|
||||
const getGroupAtUserBaseInfo = async () => {
|
||||
if (currentRoomId.value === '1') return
|
||||
currentAtUsersList.value = await apis.getAllUserBaseInfo({ roomId: currentRoomId.value })
|
||||
if (currentRoomId.value === '1' || currentRoomId.value == null) return
|
||||
const data: any = await invokeWithErrorHandler(
|
||||
TauriCommand.GET_ROOM_MEMBERS,
|
||||
{
|
||||
roomId: currentRoomId.value.toString()
|
||||
},
|
||||
{
|
||||
customErrorMessage: '获取群组成员失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
)
|
||||
// 更新 groupStore 中的 userList
|
||||
groupStore.userList = data
|
||||
currentAtUsersList.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,6 +298,13 @@ export const useCachedStore = defineStore(StoresEnum.CACHED, () => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateMyRoomInfo = async (data: any) => {
|
||||
await invokeSilently(TauriCommand.UPDATE_MY_ROOM_INFO, {
|
||||
myRoomInfo: data
|
||||
})
|
||||
await getGroupAtUserBaseInfo()
|
||||
}
|
||||
|
||||
return {
|
||||
userCachedList,
|
||||
badgeCachedList,
|
||||
@@ -287,6 +320,7 @@ export const useCachedStore = defineStore(StoresEnum.CACHED, () => {
|
||||
updateUserGroupNickname,
|
||||
getUserGroupNickname,
|
||||
userGroupNicknameMap,
|
||||
getGroupAnnouncementList
|
||||
getGroupAnnouncementList,
|
||||
updateMyRoomInfo
|
||||
}
|
||||
})
|
||||
|
||||
+40
-33
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { useRoute } from 'vue-router'
|
||||
import apis from '@/services/apis'
|
||||
import type { MarkItemType, MessageType, RevokedMsgType, SessionItem } from '@/services/types'
|
||||
import { MessageStatusEnum, MsgEnum, NotificationTypeEnum, RoomTypeEnum, StoresEnum } from '@/enums'
|
||||
import { MessageStatusEnum, MsgEnum, NotificationTypeEnum, RoomTypeEnum, StoresEnum, TauriCommand } from '@/enums'
|
||||
import { computedTimeBlock } from '@/utils/ComputedTime.ts'
|
||||
import { useCachedStore } from '@/stores/cached.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
@@ -12,7 +12,9 @@ import { cloneDeep } from 'lodash-es'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { renderReplyContent } from '@/utils/RenderReplyContent.ts'
|
||||
import { sendNotification } from '@tauri-apps/plugin-notification'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invokeWithErrorHandler, invokeSilently } from '@/utils/TauriInvokeHandler'
|
||||
import { ErrorType } from '@/common/exception'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
|
||||
type RecalledMessage = {
|
||||
messageId: string
|
||||
@@ -45,6 +47,7 @@ export const useChatStore = defineStore(
|
||||
StoresEnum.CHAT,
|
||||
() => {
|
||||
const route = useRoute()
|
||||
// const router = useRouter()
|
||||
const cachedStore = useCachedStore()
|
||||
const userStore = useUserStore()
|
||||
const globalStore = useGlobalStore()
|
||||
@@ -152,8 +155,12 @@ export const useChatStore = defineStore(
|
||||
})
|
||||
|
||||
// 监听当前房间ID的变化
|
||||
watch(currentRoomId, (val, oldVal) => {
|
||||
if (oldVal !== undefined && val !== oldVal) {
|
||||
watch(currentRoomId, async (val, oldVal) => {
|
||||
if (WebviewWindow.getCurrent().label === 'login') {
|
||||
return
|
||||
}
|
||||
|
||||
if (val !== oldVal) {
|
||||
// 1. 立即清空当前消息列表
|
||||
if (currentMessageMap.value) {
|
||||
currentMessageMap.value.clear()
|
||||
@@ -189,7 +196,6 @@ export const useChatStore = defineStore(
|
||||
// 群组的时候去请求
|
||||
if (currentRoomType.value === RoomTypeEnum.GROUP) {
|
||||
// 放到和公告一起加载
|
||||
// groupStore.getGroupUserList(true)
|
||||
cachedStore.getGroupAtUserBaseInfo()
|
||||
}
|
||||
|
||||
@@ -219,19 +225,25 @@ export const useChatStore = defineStore(
|
||||
const requestRoomId = currentRoomId.value
|
||||
|
||||
currentMessageOptions.value && (currentMessageOptions.value.isLoading = true)
|
||||
const data = await apis
|
||||
.getMsgList({
|
||||
pageSize: size,
|
||||
cursor: currentMessageOptions.value?.cursor,
|
||||
roomId: requestRoomId
|
||||
})
|
||||
.finally(() => {
|
||||
// 只有当当前房间ID仍然是请求时的房间ID时,才更新加载状态
|
||||
if (requestRoomId === currentRoomId.value && currentMessageOptions.value) {
|
||||
currentMessageOptions.value.isLoading = false
|
||||
const data: any = await invokeWithErrorHandler(
|
||||
TauriCommand.PAGE_MSG,
|
||||
{
|
||||
param: {
|
||||
pageSize: size,
|
||||
cursor: currentMessageOptions.value?.cursor,
|
||||
roomId: requestRoomId
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
{
|
||||
customErrorMessage: '获取消息列表失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
).finally(() => {
|
||||
// 只有当当前房间ID仍然是请求时的房间ID时,才更新加载状态
|
||||
if (requestRoomId === currentRoomId.value && currentMessageOptions.value) {
|
||||
currentMessageOptions.value.isLoading = false
|
||||
}
|
||||
})
|
||||
// 如果没有数据或者房间ID已经变化,则不处理响应
|
||||
if (!data || requestRoomId !== currentRoomId.value) return
|
||||
|
||||
@@ -274,17 +286,15 @@ export const useChatStore = defineStore(
|
||||
|
||||
// 获取会话列表
|
||||
const getSessionList = async (isFresh = false) => {
|
||||
if (!isFresh && (sessionOptions.isLast || sessionOptions.isLoading)) return
|
||||
if (sessionOptions.isLoading) return
|
||||
sessionOptions.isLoading = true
|
||||
// TODO: 这里先请求100条会话列表,后续优化
|
||||
const response = await apis
|
||||
.getSessionList({
|
||||
pageSize: sessionList.value.length > 100 ? sessionList.value.length : 100,
|
||||
cursor: isFresh || !sessionOptions.cursor ? '' : sessionOptions.cursor
|
||||
})
|
||||
.catch(() => {
|
||||
sessionOptions.isLoading = false
|
||||
})
|
||||
const response: any = await invokeWithErrorHandler(TauriCommand.LIST_CONTACTS, undefined, {
|
||||
customErrorMessage: '获取会话列表失败',
|
||||
errorType: ErrorType.Network
|
||||
}).catch(() => {
|
||||
sessionOptions.isLoading = false
|
||||
return null
|
||||
})
|
||||
if (!response) return
|
||||
const data = response
|
||||
if (!data) {
|
||||
@@ -295,10 +305,7 @@ export const useChatStore = defineStore(
|
||||
const currentSelectedRoomId = globalStore.currentSession.roomId
|
||||
|
||||
sessionList.value = []
|
||||
sessionList.value.push(...data.list)
|
||||
|
||||
sessionOptions.cursor = data.cursor
|
||||
sessionOptions.isLast = data.isLast
|
||||
sessionList.value.push(...data)
|
||||
sessionOptions.isLoading = false
|
||||
|
||||
sortAndUniqueSessionList()
|
||||
@@ -315,7 +322,7 @@ export const useChatStore = defineStore(
|
||||
// 用会话列表第一个去请求消息列表
|
||||
await getMsgList()
|
||||
// 请求第一个群成员列表
|
||||
currentRoomType.value === RoomTypeEnum.GROUP && (await groupStore.getGroupUserList(true))
|
||||
currentRoomType.value === RoomTypeEnum.GROUP && (await groupStore.getGroupUserList())
|
||||
// 初始化所有用户基本信息
|
||||
userStore.isSign && (await cachedStore.initAllUserBaseInfo())
|
||||
// 联系人列表
|
||||
@@ -691,7 +698,7 @@ export const useChatStore = defineStore(
|
||||
// 更新全局 store 中的未读计数
|
||||
globalStore.unReadMark.newMsgUnreadCount = totalUnread
|
||||
// 更新系统托盘图标上的未读数
|
||||
invoke('set_badge_count', { count: totalUnread > 0 ? totalUnread : null })
|
||||
invokeSilently('set_badge_count', { count: totalUnread > 0 ? totalUnread : null })
|
||||
}
|
||||
|
||||
// 清空所有会话的未读数
|
||||
|
||||
+12
-2
@@ -3,7 +3,8 @@ import apis from '@/services/apis'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import type { ContactItem, GroupListReq, RequestFriendItem } from '@/services/types'
|
||||
import { RequestFriendAgreeStatus } from '@/services/types'
|
||||
import { StoresEnum } from '@/enums'
|
||||
import { StoresEnum, TauriCommand } from '@/enums'
|
||||
import { invokeWithErrorHandler, ErrorType } from '@/utils/TauriInvokeHandler.ts'
|
||||
|
||||
// 定义分页大小常量
|
||||
export const pageSize = 20
|
||||
@@ -60,7 +61,16 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
|
||||
* 获取群聊列表
|
||||
*/
|
||||
const getGroupChatList = async () => {
|
||||
const response = await apis.groupList({ current: 1, size: 50 })
|
||||
const response: any = await invokeWithErrorHandler(
|
||||
TauriCommand.PAGE_ROOM,
|
||||
{
|
||||
pageParam: { current: 1, size: 50 }
|
||||
},
|
||||
{
|
||||
customErrorMessage: '获取群聊列表失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
)
|
||||
groupChatList.value = response.records
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useChatStore } from '@/stores/chat'
|
||||
import type { ContactItem, RequestFriendItem } from '@/services/types'
|
||||
import { clearQueue, readCountQueue } from '@/utils/ReadCountQueue.ts'
|
||||
import apis from '@/services/apis'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invokeSilently } from '@/utils/TauriInvokeHandler'
|
||||
|
||||
export const useGlobalStore = defineStore(
|
||||
StoresEnum.GLOBAL,
|
||||
@@ -76,7 +76,7 @@ export const useGlobalStore = defineStore(
|
||||
return total + (session.unreadCount || 0)
|
||||
}, 0)
|
||||
unReadMark.newMsgUnreadCount = totalUnread
|
||||
await invoke('set_badge_count', { count: totalUnread > 0 ? totalUnread : null })
|
||||
await invokeSilently('set_badge_count', { count: totalUnread > 0 ? totalUnread : null })
|
||||
}
|
||||
|
||||
// 监听当前会话变化
|
||||
|
||||
+17
-36
@@ -2,32 +2,12 @@ import apis from '@/services/apis'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import type { GroupDetailReq, UserItem } from '@/services/types'
|
||||
import { pageSize, useChatStore } from './chat'
|
||||
import { OnlineEnum, RoleEnum, RoomTypeEnum, StoresEnum } from '@/enums'
|
||||
import { uniqueUserList } from '@/utils/Unique.ts'
|
||||
import { useChatStore } from './chat'
|
||||
import { RoleEnum, RoomTypeEnum, StoresEnum, TauriCommand } from '@/enums'
|
||||
import { useCachedStore } from '@/stores/cached'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { OnStatusChangeType } from '@/services/wsType'
|
||||
|
||||
/**
|
||||
* 用户排序函数
|
||||
* 排序规则:
|
||||
* 1. 在线用户优先显示
|
||||
* 2. 同为在线或离线状态时,按最后操作时间倒序排序
|
||||
*/
|
||||
const sorAction = (pre: UserItem, next: UserItem) => {
|
||||
if (pre.activeStatus === OnlineEnum.ONLINE && next.activeStatus === OnlineEnum.ONLINE) {
|
||||
return next.lastOptTime < pre.lastOptTime ? -1 : 1
|
||||
} else if (pre.activeStatus !== OnlineEnum.ONLINE && next.activeStatus !== OnlineEnum.ONLINE) {
|
||||
return next.lastOptTime < pre.lastOptTime ? -1 : 1
|
||||
} else if (pre.activeStatus === OnlineEnum.ONLINE && next.activeStatus !== OnlineEnum.ONLINE) {
|
||||
return -1
|
||||
} else if (pre.activeStatus !== OnlineEnum.ONLINE && next.activeStatus === OnlineEnum.ONLINE) {
|
||||
return 1
|
||||
} else {
|
||||
return next.lastOptTime < pre.lastOptTime ? -1 : 1
|
||||
}
|
||||
}
|
||||
import { invokeWithErrorHandler, ErrorType } from '@/utils/TauriInvokeHandler.ts'
|
||||
|
||||
export const useGroupStore = defineStore(StoresEnum.GROUP, () => {
|
||||
// 初始化需要使用的store
|
||||
@@ -108,19 +88,20 @@ export const useGroupStore = defineStore(StoresEnum.GROUP, () => {
|
||||
|
||||
/**
|
||||
* 获取群成员列表
|
||||
* @param refresh 是否刷新(重新加载)
|
||||
*/
|
||||
const getGroupUserList = async (refresh = false, specifiedRoomId?: string) => {
|
||||
const data = await apis.getGroupList({
|
||||
pageSize: pageSize,
|
||||
cursor: refresh ? '' : userListOptions.cursor,
|
||||
roomId: specifiedRoomId || currentRoomId.value
|
||||
})
|
||||
const getGroupUserList = async () => {
|
||||
const data: any = await invokeWithErrorHandler(
|
||||
TauriCommand.GET_ROOM_MEMBERS,
|
||||
{
|
||||
roomId: currentRoomId.value
|
||||
},
|
||||
{
|
||||
customErrorMessage: '获取群成员列表失败',
|
||||
errorType: ErrorType.Network
|
||||
}
|
||||
)
|
||||
if (!data) return
|
||||
// 合并并去重用户列表,然后按在线状态和时间排序
|
||||
const newUserList = uniqueUserList(refresh ? data.list : [...data.list, ...userList.value])
|
||||
newUserList.sort(sorAction)
|
||||
userList.value = newUserList
|
||||
userList.value = data
|
||||
userListOptions.cursor = data.cursor
|
||||
userListOptions.isLast = data.isLast
|
||||
userListOptions.loading = false
|
||||
@@ -220,11 +201,11 @@ export const useGroupStore = defineStore(StoresEnum.GROUP, () => {
|
||||
*/
|
||||
const refreshGroupMembers = async () => {
|
||||
// 始终刷新频道成员列表
|
||||
await getGroupUserList(true, '1')
|
||||
await getGroupUserList()
|
||||
|
||||
// 如果当前选中的是群聊且不是频道,则同时刷新当前群聊的成员列表
|
||||
if (globalStore.currentSession?.type === RoomTypeEnum.GROUP && currentRoomId.value !== '1') {
|
||||
await getGroupUserList(true, currentRoomId.value)
|
||||
await getGroupUserList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { AppException, ErrorType } from '@/common/exception'
|
||||
|
||||
/**
|
||||
* Tauri invoke 调用的统一错误处理包装器
|
||||
* @param command Tauri 命令名称
|
||||
* @param args 命令参数
|
||||
* @param options 错误处理选项
|
||||
* @returns Promise<T>
|
||||
*/
|
||||
export async function invokeWithErrorHandler<T = any>(
|
||||
command: string,
|
||||
args?: Record<string, any>,
|
||||
options?: {
|
||||
/** 是否显示错误提示,默认为 true */
|
||||
showError?: boolean
|
||||
/** 自定义错误消息 */
|
||||
customErrorMessage?: string
|
||||
/** 是否为重试相关的错误,默认为 false */
|
||||
isRetryError?: boolean
|
||||
/** 错误类型,默认为 Unknown */
|
||||
errorType?: ErrorType
|
||||
}
|
||||
): Promise<T> {
|
||||
const { showError = true, customErrorMessage, isRetryError = false, errorType = ErrorType.Unknown } = options || {}
|
||||
|
||||
try {
|
||||
const result = await invoke<T>(command, args)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(`[Tauri Invoke Error] 命令: ${command}`, error)
|
||||
|
||||
// 构造错误消息
|
||||
let errorMessage = customErrorMessage
|
||||
if (!errorMessage) {
|
||||
if (typeof error === 'string') {
|
||||
errorMessage = error
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
} else {
|
||||
errorMessage = `调用 ${command} 命令失败`
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 AppException 统一处理错误
|
||||
throw new AppException(errorMessage, {
|
||||
type: errorType,
|
||||
showError,
|
||||
isRetryError,
|
||||
details: {
|
||||
command,
|
||||
args,
|
||||
originalError: error
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 静默调用 Tauri 命令(不显示错误提示)
|
||||
* @param command Tauri 命令名称
|
||||
* @param args 命令参数
|
||||
* @returns Promise<T | null> 成功返回结果,失败返回 null
|
||||
*/
|
||||
export async function invokeSilently<T = any>(command: string, args?: Record<string, any>): Promise<T | null> {
|
||||
try {
|
||||
return await invokeWithErrorHandler<T>(command, args, { showError: false })
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试机制的 Tauri 调用
|
||||
* @param command Tauri 命令名称
|
||||
* @param args 命令参数
|
||||
* @param options 重试选项
|
||||
* @returns Promise<T>
|
||||
*/
|
||||
export async function invokeWithRetry<T = any>(
|
||||
command: string,
|
||||
args?: Record<string, any>,
|
||||
options?: {
|
||||
/** 最大重试次数,默认为 3 */
|
||||
maxRetries?: number
|
||||
/** 重试间隔(毫秒),默认为 1000 */
|
||||
retryDelay?: number
|
||||
/** 是否显示错误提示,默认为 true */
|
||||
showError?: boolean
|
||||
/** 自定义错误消息 */
|
||||
customErrorMessage?: string
|
||||
}
|
||||
): Promise<T> {
|
||||
const { maxRetries = 3, retryDelay = 1000, showError = true, customErrorMessage } = options || {}
|
||||
|
||||
let lastError: any
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await invokeWithErrorHandler<T>(command, args, {
|
||||
showError: attempt === maxRetries ? showError : false,
|
||||
customErrorMessage: attempt === maxRetries ? customErrorMessage : undefined,
|
||||
isRetryError: attempt < maxRetries,
|
||||
errorType: ErrorType.Network
|
||||
})
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`🔄 重试 ${command} 命令 (${attempt}/${maxRetries})...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
export { ErrorType }
|
||||
@@ -112,7 +112,7 @@ import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { currentMonitor, PhysicalPosition } from '@tauri-apps/api/window'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invokeSilently } from '@/utils/TauriInvokeHandler.ts'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const { createWebviewWindow, resizeWindow, setResizable } = useWindow()
|
||||
@@ -293,7 +293,7 @@ const init = async () => {
|
||||
if (osType === 'macos') {
|
||||
// 隐藏标题栏按钮
|
||||
try {
|
||||
await invoke('hide_title_bar_buttons', { windowLabel: 'checkupdate' })
|
||||
await invokeSilently('hide_title_bar_buttons', { windowLabel: 'checkupdate' })
|
||||
} catch (error) {
|
||||
console.error('隐藏标题栏按钮失败:', error)
|
||||
}
|
||||
|
||||
@@ -192,6 +192,10 @@ import { clearListener } from '@/utils/ReadCountQueue'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { useCheckUpdate } from '@/hooks/useCheckUpdate'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { TauriCommand } from '~/src/enums'
|
||||
import { invokeWithErrorHandler } from '@/utils/TauriInvokeHandler'
|
||||
import { ErrorType } from '@/common/exception'
|
||||
|
||||
const isCompatibility = computed(() => type() === 'windows' || type() === 'linux')
|
||||
const settingStore = useSettingStore()
|
||||
@@ -364,6 +368,7 @@ const normalLogin = async (auto = false) => {
|
||||
// 存储双token
|
||||
localStorage.setItem('TOKEN', res.token)
|
||||
localStorage.setItem('REFRESH_TOKEN', res.refreshToken)
|
||||
|
||||
// 需要删除二维码,因为用户可能先跳转到二维码界面再回到登录界面,会导致二维码一直保持在内存中
|
||||
if (localStorage.getItem('wsLogin')) {
|
||||
localStorage.removeItem('wsLogin')
|
||||
@@ -386,16 +391,35 @@ const normalLogin = async (auto = false) => {
|
||||
const account = {
|
||||
...userDetail,
|
||||
token: res.token,
|
||||
refreshToken: res.refreshToken,
|
||||
client: res.client
|
||||
}
|
||||
userStore.userInfo = account
|
||||
loginHistoriesStore.addLoginHistory(account)
|
||||
|
||||
await invokeWithErrorHandler(
|
||||
TauriCommand.SAVE_USER_INFO,
|
||||
{
|
||||
userInfo: account
|
||||
},
|
||||
{
|
||||
customErrorMessage: '保存用户信息失败',
|
||||
errorType: ErrorType.Client
|
||||
}
|
||||
)
|
||||
|
||||
await emit('set_user_info', {
|
||||
token: res.token,
|
||||
refreshToken: res.refreshToken,
|
||||
uid: account.uid
|
||||
})
|
||||
|
||||
await setLoginState()
|
||||
await openHomeWindow()
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
console.error('登录异常:', e)
|
||||
loading.value = false
|
||||
loginDisabled.value = false
|
||||
loginText.value = '登录'
|
||||
|
||||
@@ -55,7 +55,7 @@ const filteredOptions = computed(() => getFilteredOptions())
|
||||
// 初始化群成员数据
|
||||
const initGroupMembers = async () => {
|
||||
if (globalStore.currentSession?.roomId) {
|
||||
await groupStore.getGroupUserList(true, globalStore.currentSession.roomId)
|
||||
await groupStore.getGroupUserList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { invokeWithErrorHandler, invokeSilently, ErrorType } from '@/utils/TauriInvokeHandler.ts'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { appCacheDir } from '@tauri-apps/api/path'
|
||||
@@ -273,9 +273,16 @@ const startScan = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await invoke<DirectoryInfo>('get_directory_usage_info_with_progress', {
|
||||
directoryPath: currentDirectory.value
|
||||
})
|
||||
const result = await invokeWithErrorHandler<DirectoryInfo>(
|
||||
'get_directory_usage_info_with_progress',
|
||||
{
|
||||
directoryPath: currentDirectory.value
|
||||
},
|
||||
{
|
||||
customErrorMessage: '获取目录信息失败',
|
||||
errorType: ErrorType.Client
|
||||
}
|
||||
)
|
||||
|
||||
diskInfo.value = result
|
||||
totalSize.value = result.total_size
|
||||
@@ -290,7 +297,7 @@ const startScan = async () => {
|
||||
// 取消扫描的方法
|
||||
const cancelScan = async () => {
|
||||
try {
|
||||
await invoke('cancel_directory_scan')
|
||||
await invokeSilently('cancel_directory_scan')
|
||||
console.log('扫描已取消')
|
||||
} catch (error) {
|
||||
console.error('取消扫描失败:', error)
|
||||
|
||||
Reference in New Issue
Block a user