来宾市网站建设_网站建设公司_腾讯云_seo优化
2025/12/24 23:24:34 网站建设 项目流程

Chap20-Communication

这一节拖得有点久,主要问题是涉及到的字段较多,写的时候太随意奔放导致遗留了很对字段没有设置,出现了很多莫名其妙的bug.因此,在编码的时候一定要捋清信号和槽的关系,每次点击都要处理好你我状态的设置。

本来只是想要简单的客户端请求,服务器给回复,但是考虑到对服务器的压力,因此,我们引入了本地的服务器Sqlite,用于在没有服务器连接的时候,也能存放一定的信息,同时减轻服务器的压力。也就是,能本地存,就本地存,默认不往服务器存。只有对方不在线的时候,才会存储在服务器,然后在对方上线之后,发给对方。至于本地和服务器的数据同步,也许以后会做。

数据库

服务器数据库

image-20251126082511790

user

image-20251126082604530

notifications

image-20251126082729772

messages

image-20251126082757331

friends

image-20251126082819708

friend_apply

image-20251126082850151

conversations

image-20251126082915621

本地Sqlite数据库

本地数据库直接由前端代码创建:

messages

image-20251126083342900

conversations

image-20251126083415703

friends

image-20251126083455812

可以看到本地数据库和服务器的数据库字段和结构有些不一样,比如对于服务器来说,不关心用户之间的关系,对他而言只是一个标志,对于本地数据库来说,用户是从自己的角度出发的所以考虑自己的关系,自己的好友。因此,本地数据库不存放user表,存放的是friends表,这张表也一定程度替代了user,包含了各种信息字段。

最需要注意的是,sqlite没有TIMESTAMP字段,也没有纯时间戳的类型,我们为了方便,全部使用了TEXT类型,服务器的数据库还是使用了TIMESTAMP类型,前端代码中使用的是QDateTime,方便从QDateTime->string和string到QDateTime的类型转换(QDateTime::toString("yyyy-MM-dd HH:mm:ss"),QVariant::toDateTime)。比如一个string类型的日期,存放mysql数据库,可以自动转换成TIMESTAMP,只要格式正确。因此我们使用TEXT保存的时候,需要保存的格式为“yyyy-MM-dd HH:mm:ss”.

后端

这里先记录后端,因为后端的工作量比较小,主要是CRUD.

首先有一个重要的结构:im::MessageItem用来前后端通信的protobuf,以及一个用来承载解析体的MessageItem(这个只在前端进行传递,后端没有).

syntax = "proto3";
package im;message MessageContent {int32  type         = 1;string data         = 2; // 文本可放string mime_type    = 3;string fid          = 4;
}message MessageItem {string id        = 1;int32  to_id     = 2;int32  from_id   = 3;string  timestamp = 4; // unix msint32  env       = 5;MessageContent content =6;
}
struct MessageContent{MessageType     type;           // 自定义类型QVariant        data;           // 如果是文本文件,存放在这里,如果是二进制,此为空。QString         mimeType;       // 具体的类型比如text/plainQString         fid;            // 文件服务器需要用
};struct MessageItem{QString               id;           // 唯一的消息idint                   to_id;        // 接受者idint                   from_id;      // 发送者的idQDateTime             timestamp;    // 时间MessageEnv            env;          // 私聊还是群聊MessageContent        content;      // 实际的内容串bool                  isSelected;   // 之后可能会有聊天记录的选择,删除int                   status;MessageItem():id(QUuid::createUuid().toString()),from_id(UserManager::GetInstance()->GetUid()),timestamp(QDateTime::currentDateTime()),env(MessageEnv::Private),isSelected(false),status(0){}
};

同时为了MessageItem和im::MessageItem方便转换,我们编写了静态函数,用于双向转换。

// 转成发给服务器的im::MessageItem
static im::MessageItem toPb(const MessageItem &m)
{im::MessageItem pb;pb.set_id(m.id.toStdString());pb.set_from_id(m.from_id);pb.set_to_id(m.to_id);pb.set_timestamp(m.timestamp.toString("yyyy-MM-dd HH:mm:ss").toStdString());pb.set_env(static_cast<int32_t>(m.env));qDebug() << "env!!!:" << static_cast<int>(m.env);auto* c = pb.mutable_content();c->set_type(static_cast<int32_t>(m.content.type));c->set_data(m.content.data.toString().toStdString());c->set_mime_type(m.content.mimeType.toStdString());c->set_fid(m.content.fid.toStdString());return pb;
}// 服务器收回来解析成MessageItem
static MessageItem fromPb(const im::MessageItem&pb)
{QString format = "yyyy-MM-dd HH:mm:ss";MessageItem m;m.id                = QString::fromStdString(pb.id());m.to_id             = pb.to_id();m.from_id           = pb.from_id();m.timestamp         = QDateTime::fromString(QString::fromStdString(pb.timestamp()),format);m.env               = MessageEnv(pb.env());m.content.fid       = QString::fromStdString(pb.content().fid());m.content.type      = MessageType(pb.content().type());m.content.data      = QString::fromStdString(pb.content().data());m.content.mimeType  = QString::fromStdString(pb.content().mime_type());return m;
}

ID_CHAT_LOGIN

首先是登陆的时候,我们补充了获取会话列表,获取未读消息等

// 获取会话列表std::vector<std::shared_ptr<SessionInfo>> session_list;bool b_session = MysqlManager::GetInstance()->GetSeessionList(uid_str, session_list);if (b_session && session_list.size() > 0) {json conversations;for (auto& session_item : session_list) {json conversation;conversation["uid"] = session_item->uid;conversation["from_uid"] = session_item->from_uid;conversation["to_uid"] = session_item->to_uid;conversation["create_time"] = session_item->create_time;conversation["update_time"] = session_item->update_time;conversation["name"] = session_item->name;conversation["icon"] = session_item->icon;conversation["status"] = session_item->status;conversation["deleted"] = session_item->deleted;conversation["pined"] = session_item->pined;conversations.push_back(conversation);}jj["conversations"] = conversations;}
// 获取未读消息std::vector<std::shared_ptr<im::MessageItem>> unread_messages;bool b_unread = MysqlManager::GetInstance()->GetUnreadMessages(uid_str, unread_messages);if (b_unread && unread_messages.size() > 0) {json messages = json::array();for (auto& message : unread_messages) {json message_item;message_item["id"] = message->id();message_item["from_id"] = message->from_id();message_item["to_id"] = message->to_id();message_item["timestamp"] = message->timestamp();message_item["env"] = message->env();message_item["content_type"] = message->content().type();message_item["content_data"] = message->content().data();message_item["content_mime_type"] = message->content().mime_type();message_item["content_fid"] = message->content().fid();messages.push_back(message_item);}jj["unread_messages"] = messages;}

ID_AUTH_FRIEND_REQ

新修补了一些bug,比如对方没有同意好友信息,这个通知发给申请人之后,点击确认,服务器仍然没有改变状态,下次登陆还是会发送。

if (j.contains("reply")) {bool b = j["reply"].get<bool>();if (b) {// 只是收到通知回复,我们把数据库状态更新一下// 如果失败说明当前双方都在线,消息就没有入库,所以这里不做处理。auto fromUid = j["from_uid"].get<int>();bool ok1 = MysqlManager::GetInstance()->ChangeMessageStatus(std::to_string(fromUid), 1);return;}
}

这里不管对方是否同意,只有是一条回复,同时申请人确认收到,那么就更改状态,下次就不会再次发送过来了。

ID_TEXT_CHAT_MSG_REQ

这个是服务器处理对方发来的protobuf格式的消息的重要回调。

但是思路照常,当to_uid的用户在线,我们先看是否这个用户在我们的服务器下,是,我们直接发送过去,否,grpc传给其他服务器再发送给用户。否则的话,我们就服务器把这条消息存储下来,直到用户下次登陆,再传给用户。

同时由于双方都使用的protobuf,服务器在接收到消息之后,除了数据库存储需要获取消息的信息,其他情况下就是直接转发即可,无需类似json解析。

_function_callbacks[MsgId::ID_TEXT_CHAT_MSG_REQ] = [this](std::shared_ptr<Session> session, uint16_t msg_id, const std::string& msg) {json j;j["error"] = ErrorCodes::SUCCESS;Defer defer([this, &j, session]() {session->Send(j.dump(), static_cast<int>(MsgId::ID_TEXT_CHAT_MSG_RSP));});im::MessageItem pb;pb.ParseFromString(msg);auto& cfg = ConfigManager::GetInstance();auto self_name = cfg["SelfServer"]["name"];auto to_uid = pb.to_id();std::string to_key = USERIP_PREFIX + std::to_string(to_uid);std::string to_ip_value;bool b_ip = RedisManager::GetInstance()->Get(to_key, to_ip_value);if (!b_ip) {// 当前不在线bool ok = MysqlManager::GetInstance()->AddMessage(pb.id(), pb.from_id(), pb.to_id(), pb.timestamp(), pb.env(), pb.content().type(), pb.content().data(), pb.content().mime_type(), pb.content().fid(), 0);return;} else {if (to_ip_value == self_name) {auto session2 = UserManager::GetInstance()->GetSession(to_uid);if (session2) {SPDLOG_INFO("FROM UID:{},to:{}", pb.from_id(), to_uid);SPDLOG_INFO("FROM SESSION:{},to:{}", session->GetSessionId(), session2->GetSessionId());session2->Send(msg, static_cast<int>(MsgId::ID_TEXT_CHAT_MSG_REQ));bool ok = MysqlManager::GetInstance()->AddMessage(pb.id(), pb.from_id(), pb.to_id(), pb.timestamp(), pb.env(), pb.content().type(), pb.content().data(), pb.content().mime_type(), pb.content().fid(), 1);}} else {TextChatMessageRequest req;req.set_fromuid(pb.from_id());req.set_touid(pb.to_id());req.set_data(msg);ChatGrpcClient::GetInstance()->NotifyTextChatMessage(to_ip_value, req);}}};

ID_SYNC_CONVERSATIONS_REQ

如名其意,这个回调的作用是进行消息的同步,虽然现在编写了这个函数,但是客户端还没有决定好同步时机和策略,但是不考虑,仅贴出代码:

_function_callbacks[MsgId::ID_SYNC_CONVERSATIONS_REQ] = [this](std::shared_ptr<Session> session, uint16_t msg_id, const std::string& msg) {json j;try {j = json::parse(msg);} catch (const std::exception& e) {SPDLOG_WARN("SyncConversations parse error: {}", e.what());json err;err["error"] = ErrorCodes::ERROR_JSON;return;}if (!j.contains("conversations") || !j["conversations"].is_array()) {SPDLOG_WARN("SyncConversations missing conversations array");return;}// 所属用户 uid(客户端会发送)int owner_uid = j.value("uid", 0);std::string owner_uid_str = std::to_string(owner_uid);for (const auto& item : j["conversations"]) {try {auto conv = std::make_shared<SessionInfo>();conv->uid = item.value("uid", 0);conv->from_uid = item.value("from_uid", 0);conv->to_uid = item.value("to_uid", 0);conv->create_time = item.value("create_time", std::string());conv->update_time = item.value("update_time", std::string());conv->name = item.value("name", std::string());conv->icon = item.value("icon", std::string());conv->status = item.value("status", 0);conv->deleted = item.value("deleted", 0);conv->pined = item.value("pined", 0);conv->processed = item.value("processed", false);// 客户端可能携带本地 processed 字段,用于 UI,本段不用写入 DB// 将会话写入数据库// 假定 MysqlManager 提供 AddConversation(owner_uid, std::shared_ptr<SessionInfo>)// 如果项目中签名不同,请根据实际签名调整此处调用。bool ok = MysqlManager::GetInstance()->AddConversation(conv->uid, conv->from_uid, conv->to_uid, conv->create_time, conv->update_time, conv->name, conv->icon, conv->status, conv->deleted, conv->pined, conv->processed);if (!ok) {SPDLOG_WARN("AddConversation failed owner:{} conv_uid:{}", owner_uid, conv->uid);// 不中断,继续处理剩余会话}} catch (const std::exception& e) {SPDLOG_WARN("Exception when processing conversation item: {}", e.what());// 继续处理下一个}}};

数据库

对于数据库而言,一般都是增删改查,因此我们在上面的回调没有解释,仅仅通过名称就能判断作用。这里给出代码:

// MysqlManager.h
bool GetFriendList(const std::string& uid, std::vector<std::shared_ptr<UserInfo>>&);/*** @brief 添加消息入库** @return true* @return false*/
bool ChangeMessageStatus(const std::string& uid, int status);/*** @brief 建立好友关系** @param fromUid* @param toUid* @return true* @return false*/bool MakeFriends(const std::string& fromUid, const std::string& toUid);/*** @brief 检查是否是好友关系** @param fromUid* @param toUid* @return true* @return false*/bool CheckIsFriend(const std::string& fromUid, const std::string& toUid);/*** @brief 添加通知** @param uid* @param type* @param message* @return true* @return false*/bool AddNotification(const std::string& uid, int type, const std::string& message);/*** @brief 获取通知列表** @param uid* @param notificationList* @return true* @return false*/bool GetNotificationList(const std::string& uid, std::vector<std::shared_ptr<UserInfo>>& notificationList);/*** @brief 返回好友列表** @param uid* @return true* @return false*/bool GetFriendList(const std::string& uid, std::vector<std::shared_ptr<UserInfo>>&);/*** @brief 添加消息入库** @return true* @return false*/bool AddMessage(const std::string& uid, int from_uid, int to_uid, const std::string& timestamp, int env, int content_type, const std::string& content_data, const std::string& content_mime_type, const std::string& fid, int status = 0);/*** @brief 添加会话** @param uid* @param from_uid* @param to_uid* @param create_time* @param update_time* @param name* @param icon* @param staus* @param deleted* @param pined* @return true* @return false*/bool AddConversation(const std::string& uid, int from_uid, int to_uid, const std::string& create_time, const std::string& update_time, const std::string& name, const std::string& icon, int staus, int deleted, int pined, bool processed);/*** @brief 获取会话列表** @param uid* @param sessionList* @return true* @return false*/bool GetSeessionList(const std::string& uid, std::vector<std::shared_ptr<SessionInfo>>& sessionList);/*** @brief 获取未读取的消息** @param uid* @param unreadMessages* @return true* @return false*/bool GetUnreadMessages(const std::string& uid, std::vector<std::shared_ptr<im::MessageItem>>& unreadMessages);// MysqlDao.cpp
bool MysqlDao::GetFriendList(const std::string& uid, std::vector<std::shared_ptr<UserInfo>>& friendList)
{auto conn = _pool->GetConnection();if (!conn) {SPDLOG_ERROR("Failed to get connection from pool");return false;}Defer defer([this, &conn]() {_pool->ReturnConnection(std::move(conn));});try {mysqlpp::Query query = conn->query();// 使用显式JOIN,更清晰query << "SELECT u.uid, u.name, u.icon, u.email, u.sex, u.desc,u.back"<< " FROM user u"<< " INNER JOIN friends f ON u.uid = f.friend_id"<< " WHERE f.self_id = %0q"<< " ORDER BY f.friend_id DESC";query.parse();mysqlpp::StoreQueryResult res = query.store(std::stoi(uid));int count = res.num_rows();if (res && res.num_rows() > 0) {friendList.reserve(res.num_rows()); // 预分配内存for (size_t i = 0; i < res.num_rows(); ++i) {auto user_info = std::make_shared<UserInfo>();user_info->uid = res[i]["uid"];user_info->sex = res[i]["sex"];user_info->name = ValueOrEmpty(std::string(res[i]["name"]));user_info->icon = ValueOrEmpty(std::string(res[i]["icon"]));user_info->email = ValueOrEmpty(std::string(res[i]["email"]));user_info->desc = ValueOrEmpty(std::string(res[i]["desc"]));user_info->back = ValueOrEmpty(std::string(res[i]["back"]));friendList.push_back(user_info);}return true;}return false;} catch (const mysqlpp::Exception& e) {SPDLOG_ERROR("MySQL++ exception: {}", e.what());return false;} catch (const std::exception& e) {SPDLOG_ERROR("Exception: {}", e.what());return false;}
}
bool MysqlDao::AddMessage(const std::string& uid, int from_uid, int to_uid, const std::string& timestamp, int env, int content_type, const std::string& content_data, const std::string& content_mime_type, const std::string& content_fid, int status)
{auto conn = _pool->GetConnection();if (!conn) {SPDLOG_ERROR("Failed to get connection from pool");return false;}Defer defer([this, &conn]() {_pool->ReturnConnection(std::move(conn));});try {mysqlpp::Query query = conn->query();query << "INSERT INTO messages (uid,from_uid,to_uid,timestamp,env,content_type,content_data,content_mime_type,content_fid,status) VALUES(%0q,%1q,%2q,%3q,%4q,%5q,%6q,%7q,%8q,%9q)";query.parse();mysqlpp::SimpleResult res = query.execute(uid, from_uid, to_uid, timestamp, env, content_type, content_data, content_mime_type, content_fid, status);if (res) {int affected_rows = res.rows();if (affected_rows > 0) {SPDLOG_INFO("Message added successfully for from_uid: {}, to_uid: {}, timestamp: {}, env: {}, content_type: {}, content_data: {}, content_mime_type: {}, fid: {}, status: {}", from_uid, to_uid, timestamp, env, content_type, content_data, content_mime_type, content_fid, status);return true;} else {SPDLOG_WARN("Failed to add message for from_uid: {}, to_uid: {}, timestamp: {}, env: {}, content_type: {}, content_data: {}, content_mime_type: {}, fid: {}, status: {}", from_uid, to_uid, timestamp, env, content_type, content_data, content_mime_type, content_fid, status);return false;}} else {SPDLOG_ERROR("Failed to add message: {}", query.error());return false;}} catch (const mysqlpp::Exception& e) {SPDLOG_ERROR("MySQL++ exception: {}", e.what());return false;} catch (const std::exception& e) {SPDLOG_ERROR("Exception: {}", e.what());return false;}
}bool MysqlDao::AddConversation(const std::string& uid, int from_uid, int to_uid, const std::string& create_time, const std::string& update_time, const std::string& name, const std::string& icon, int staus, int deleted, int pined, bool processed)
{auto conn = _pool->GetConnection();if (!conn) {SPDLOG_ERROR("Failed to get connection from pool");return false;}Defer defer([this, &conn]() {_pool->ReturnConnection(std::move(conn));});try {mysqlpp::Query query = conn->query();query << "INSERT INTO conversations (uid,from_uid,to_uid,create_time,update_time,name,icon,status,deleted,pined,processed) VALUES(%0q,%1q,%2q,%3q,%4q,%5q,%6q,%7q,%8q,%9q,%10)";query.parse();mysqlpp::SimpleResult res = query.execute(uid, from_uid, to_uid, create_time, update_time, name, icon, staus, deleted, pined, processed);if (res) {int affected_rows = res.rows();if (affected_rows > 0) {SPDLOG_INFO("Conversation added successfully for uid: {}, from_uid: {}, to_uid: {}, create_time: {}, update_time: {}, name: {}, icon: {}, status: {}, deleted: {}, pined: {}", uid, from_uid, to_uid, create_time, update_time, name, icon, staus, deleted, pined);return true;} else {SPDLOG_WARN("Failed to add conversation for uid: {}, from_uid: {}, to_uid: {}, create_time: {}, update_time: {}, name: {}, icon: {}, status: {}, deleted: {}, pined: {}", uid, from_uid, to_uid, create_time, update_time, name, icon, staus, deleted, pined);return false;}} else {SPDLOG_ERROR("Failed to add conversation: {}", query.error());return false;}} catch (const mysqlpp::Exception& e) {SPDLOG_ERROR("MySQL++ exception: {}", e.what());return false;} catch (const std::exception& e) {SPDLOG_ERROR("Exception: {}", e.what());return false;}
}bool MysqlDao::GetSeessionList(const std::string& uid, std::vector<std::shared_ptr<SessionInfo>>& sessionList)
{auto conn = _pool->GetConnection();if (!conn) {SPDLOG_ERROR("Failed to get connection from pool");return false;}Defer defer([this, &conn]() {_pool->ReturnConnection(std::move(conn));});try {mysqlpp::Query query = conn->query();query << "SELECT * FROM conversations"<< " WHERE (from_uid = %0q AND deleted = 0)";query.parse();mysqlpp::StoreQueryResult res = query.store(std::stoi(uid));int count = res.num_rows();if (res && res.num_rows() > 0) {sessionList.reserve(res.num_rows()); // 预分配内存for (size_t i = 0; i < res.num_rows(); ++i) {auto session_info = std::make_shared<SessionInfo>();session_info->uid = res[i]["uid"].c_str();session_info->from_uid = res[i]["from_uid"];session_info->to_uid = res[i]["to_uid"];session_info->create_time = res[i]["create_time"].c_str();session_info->update_time = res[i]["update_time"].c_str();session_info->name = ValueOrEmpty(std::string(res[i]["name"]));session_info->icon = ValueOrEmpty(std::string(res[i]["icon"]));session_info->status = res[i]["status"];session_info->deleted = res[i]["deleted"];session_info->pined = res[i]["pined"];session_info->processed = res[i]["processed"];sessionList.push_back(session_info);}return true;}return false;} catch (const mysqlpp::Exception& e) {SPDLOG_ERROR("MySQL++ exception: {}", e.what());return false;} catch (const std::exception& e) {SPDLOG_ERROR("Exception: {}", e.what());return false;}
}bool MysqlDao::GetUnreadMessages(const std::string& uid, std::vector<std::shared_ptr<im::MessageItem>>& unreadMessages)
{auto conn = _pool->GetConnection();if (!conn) {SPDLOG_ERROR("Failed to get connection from pool");return false;}Defer defer([this, &conn]() {_pool->ReturnConnection(std::move(conn));});try {mysqlpp::Query query = conn->query();query << "SELECT * FROM messages"<< " WHERE to_uid = %0q AND status = 0";query.parse();mysqlpp::StoreQueryResult res = query.store(std::stoi(uid));int count = res.num_rows();if (res && res.num_rows() > 0) {unreadMessages.reserve(res.num_rows()); // 预分配内存for (size_t i = 0; i < res.num_rows(); ++i) {auto message_item = std::make_shared<im::MessageItem>();message_item->set_id(res[i]["uid"].c_str());message_item->set_from_id(res[i]["from_uid"]);message_item->set_to_id(res[i]["to_uid"]);message_item->set_timestamp(res[i]["timestamp"].c_str());message_item->set_env(res[i]["env"]);message_item->mutable_content()->set_type(res[i]["content_type"]);message_item->mutable_content()->set_data(res[i]["content_data"].c_str());message_item->mutable_content()->set_mime_type(res[i]["content_mime_type"].c_str());message_item->mutable_content()->set_fid(res[i]["content_fid"].c_str());unreadMessages.push_back(message_item);}return true;}return false;} catch (const mysqlpp::Exception& e) {SPDLOG_ERROR("MySQL++ exception: {}", e.what());return false;} catch (const std::exception& e) {SPDLOG_ERROR("Exception: {}", e.what());return false;}
}

前端

工作量很大,不可能一一记录,记录主要的流程和思路。

本地数据库

这里主要分为三个板块,跟三个数据库表对应,包括创建表,存储,读取,修改。

// database.h
#ifndef DATABASE_H
#define DATABASE_H#include <QSqlDatabase>
#include <vector>
#include <memory>
#include "MainInterface/Chat/ChatArea/MessageArea/messagetypes.h"
#include "../Properties/signalrouter.h"class DataBase
{
public:static DataBase&GetInstance();// 聊天记录bool initialization(const QString&db_path = "");bool createMessagesTables();bool storeMessage(const MessageItem&message);bool storeMessages(const std::vector<MessageItem>&messages);bool storeMessages(const std::vector<std::shared_ptr<MessageItem>>&messages);std::vector<MessageItem>getMessages(int peerUid, QString sinceTimestamp = 0,int limit = 20);bool updateMessageStatus(int messageId,int status);bool updateMessagesStatus(int peerUid, int status);bool deleteMessage(int messageId);MessageItem createMessageFromQuery(const QSqlQuery& query);// 会话列表bool createConversationTable();bool createOrUpdateConversation(const ConversationItem& conv);bool existConversation(int peerUid);bool createOrUpdateConversations(const std::vector<ConversationItem>&conversations);bool createOrUpdateConversations(const std::vector<std::shared_ptr<ConversationItem>>&conversations);std::vector<ConversationItem> getConversationList();std::vector<std::shared_ptr<ConversationItem>> getConversationListPtr();ConversationItem getConversation(int peerUid);ConversationItem createConversationFromQuery(const QSqlQuery& query);QString getLastMessage(int peerUid);// 好友列表bool createFriendsTable();std::shared_ptr<UserInfo>getFriendInfoPtr(int peerUid);UserInfo getFriendInfo(int peerUid);std::vector<UserInfo>getFriends();std::vector<std::shared_ptr<UserInfo>>getFriendsPtr();bool storeFriends(const std::vector<std::shared_ptr<UserInfo>>friends);bool storeFriends(const std::vector<UserInfo>friends);bool storeFriend(const UserInfo&info);bool storeFriend(const std::shared_ptr<UserInfo>&info);UserInfo createFriendInfoFromQuery(const QSqlQuery& query);private:DataBase() = default;
private:QString _db_path;QSqlDatabase _db;
};#endif // DATABASE_H// database.cpp
#include "database.h"#include <QDir>
#include <QStandardPaths>
#include <QSqlError>
#include <QSqlQuery>DataBase &DataBase::GetInstance()
{static DataBase db;return db;
}bool DataBase::initialization(const QString &db_path)
{if (_db.isOpen()){return true;}_db_path = db_path.isEmpty() ?QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/chat_data.db":db_path;// qDebug() <<_db_path;// 确保目录存在QDir().mkpath(QFileInfo(_db_path).absolutePath());_db = QSqlDatabase::addDatabase("QSQLITE","chat_connection");_db.setDatabaseName(_db_path);if (!_db.open()) {qDebug() << "Failed To Open Database:" << _db.lastError().text();return false;}return createMessagesTables() && createConversationTable() && createFriendsTable();
}bool DataBase::createMessagesTables()
{QSqlQuery query(_db);QString query_str ="CREATE TABLE IF NOT EXISTS messages (""id                INTEGER PRIMARY KEY AUTOINCREMENT,""uid               TEXT    NOT NULL,""owner             INTEGER NOT NULL,""from_uid          INTEGER NOT NULL,""to_uid            INTEGER NOT NULL,""timestamp         TEXT,"  // 毫秒时间戳"env               INTEGER NOT NULL,"  // 0=Private 1=Group"content_type      INTEGER NOT NULL,"  // MessageType 枚举"content_data      TEXT    NOT NULL,"  // 文本或缩略图 base64"content_mime_type TEXT,"              // 可为空"content_fid       TEXT,"              // 可为空"status            INTEGER NOT NULL DEFAULT 0" // 0=正常 1=撤回 ...")";if (!query.exec(query_str)){qDebug() << "Failed to create table:" << query.lastError().text();return false;}// 创建索引QStringList indexes = {"CREATE INDEX IF NOT EXISTS idx_from_uid ON messages(from_uid)","CREATE INDEX IF NOT EXISTS idx_to_uid ON messages(to_uid)","CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp)","CREATE INDEX IF NOT EXISTS idx_from_timestamp ON messages(from_uid, timestamp)","CREATE INDEX IF NOT EXISTS idx_to_timestamp ON messages(to_uid, timestamp)"};for (const QString& sql : indexes) {if (!QSqlQuery(_db).exec(sql)) {qDebug() << "Failed to create index:" << _db.lastError().text();}}return true;
}bool DataBase::storeMessage(const MessageItem &message)
{QSqlQuery query(_db);query.prepare(R"(INSERT INTO messages(uid,from_uid,to_uid,timestamp,env,content_type,content_data,content_mime_type,content_fid,status,owner)values(?,?,?,?,?,?,?,?,?,?,?))");query.addBindValue(message.id);query.addBindValue(message.from_id);query.addBindValue(message.to_id);query.addBindValue(message.timestamp.toString("yyyy-MM-dd HH:mm:ss"));query.addBindValue(static_cast<int>(message.env));query.addBindValue(static_cast<int>(message.content.type));query.addBindValue(message.content.data);query.addBindValue(message.content.mimeType);query.addBindValue(message.content.fid);query.addBindValue(0);query.addBindValue(UserManager::GetInstance()->GetUid());if (!query.exec()){qDebug() << "Failed to store message:" << query.lastError().text();return false;}return true;
}bool DataBase::storeMessages(const std::vector<MessageItem> &messages)
{if (messages.empty()){return true;}if (!_db.transaction()){qDebug() << "Transaction Start Error:" << _db.lastError().text();return false;}QSqlQuery query(_db);query.prepare(R"(INSERT INTO messages(uid,from_uid,to_uid,timestamp,env,content_type,content_data,content_mime_type,content_fid,status,owner)values(?,?,?,?,?,?,?,?,?,?,?))");for (const MessageItem&message:messages){query.addBindValue(message.id);query.addBindValue(message.from_id);query.addBindValue(message.to_id);query.addBindValue(message.timestamp.toString("yyyy-MM-dd HH:mm:ss"));query.addBindValue(static_cast<int>(message.env));query.addBindValue(static_cast<int>(message.content.type));query.addBindValue(message.content.data);query.addBindValue(message.content.mimeType);query.addBindValue(message.content.fid);query.addBindValue(message.status);query.addBindValue(UserManager::GetInstance()->GetUid());}if (!query.execBatch()){qDebug() << "ExecBatch Error:" << query.lastError().text();_db.rollback();return false;}if (!_db.commit()){qDebug() << "Commit Error:" << _db.lastError().text();_db.rollback();return false;}return true;
}bool DataBase::storeMessages(const std::vector<std::shared_ptr<MessageItem>> &messages)
{if (messages.empty()){return true;}if (!_db.transaction()){qDebug() << "Transaction Start Error:" << _db.lastError().text();return false;}QSqlQuery query(_db);query.prepare(R"(INSERT INTO messages(uid, from_uid, to_uid, timestamp, env, content_type, content_data, content_mime_type, content_fid, status,owner)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?))");for (const auto& message_ptr : messages){const MessageItem& message = *message_ptr;  // 解引用 shared_ptrquery.addBindValue(message.id);query.addBindValue(message.from_id);query.addBindValue(message.to_id);query.addBindValue(message.timestamp.toString("yyyy-MM-dd HH:mm:ss"));query.addBindValue(static_cast<int>(message.env));query.addBindValue(static_cast<int>(message.content.type));query.addBindValue(message.content.data);query.addBindValue(message.content.mimeType);query.addBindValue(message.content.fid);query.addBindValue(message.status);query.addBindValue(UserManager::GetInstance()->GetUid());}if (!query.execBatch()){qDebug() << "ExecBatch Error (shared_ptr version):" << query.lastError().text();_db.rollback();return false;}if (!_db.commit()){qDebug() << "Commit Error:" << _db.lastError().text();_db.rollback();return false;}qDebug() << "Successfully stored" << messages.size() << "messages (shared_ptr version)";return true;
}
std::vector<MessageItem> DataBase::getMessages(int peerUid,  QString sinceTimestamp,int limit)
{std::vector<MessageItem>messages;QSqlQuery query(_db);QString sql = R"(SELECT * FROM messagesWHERE ((from_uid = ? AND to_uid = ?) OR (from_uid = ? AND to_uid = ?)) AND owner = ?)";QVariantList params;params << peerUid << UserManager::GetInstance()->GetUid() << UserManager::GetInstance()->GetUid() << peerUid << UserManager::GetInstance()->GetUid();if (!sinceTimestamp.isEmpty()){sql += "AND timestamp < ? ";params << sinceTimestamp;}sql += "ORDER BY timestamp desc ";if (limit > 0){sql+="LIMIT ?";params << limit;}query.prepare(sql);for(int i = 0;i<params.size();++i){query.addBindValue(params[i]);}if (!query.exec()){qDebug() << "Failed To Get Messages:" << query.lastError().text();return messages;}int count = 0;QDateTime last_time;while(query.next()){messages.push_back(createMessageFromQuery(query));count++;last_time = query.value("timestamp").toDateTime();}if (count > 0){emit SignalRouter::GetInstance().on_change_last_time(peerUid,last_time);}if (count<limit){UserManager::GetInstance()->setMessagesFinished(peerUid);}return messages;
}bool DataBase::updateMessageStatus(int messageId, int status)
{QSqlQuery query(_db);query.prepare("UPDATE messages SET status = ? WHERE to_uid = ?");query.addBindValue(status);query.addBindValue(messageId);if (!query.exec()) {qDebug() << "Failed to update message status:" << query.lastError().text();return false;}if (query.numRowsAffected() == 0) {qDebug() << "updateMessageStatus::No message found with id:" << messageId;return false;}return true;
}bool DataBase::updateMessagesStatus(int peerUid, int status)
{QSqlQuery query(_db);query.prepare("UPDATE messages SET status = ? WHERE to_uid = ?");query.addBindValue(status);query.addBindValue(peerUid);if (!query.exec()) {qDebug() << "Failed to update message status:" << query.lastError().text();return false;}if (query.numRowsAffected() == 0) {qDebug() << "updateMessagesStatus::No message found with id:" << peerUid;return false;}return true;
}bool DataBase::deleteMessage(int messageId)
{QSqlQuery query(_db);query.prepare("DELETE FROM messages WHERE uid = ?");query.addBindValue(messageId);if (!query.exec()) {qDebug() << "Failed to delete message:" << query.lastError().text();return false;}if (query.numRowsAffected() == 0) {qDebug() << "deleteMessage::No message found with id:" << messageId;return false;}qDebug() << "Deleted message:" << messageId;return true;
}MessageItem DataBase::createMessageFromQuery(const QSqlQuery &query)
{MessageItem msg;msg.id = query.value("uid").toString();msg.from_id = query.value("from_uid").toInt();msg.to_id = query.value("to_uid").toInt();msg.timestamp = query.value("timestamp").toDateTime();msg.env = MessageEnv(query.value("env").toInt());msg.content.type = MessageType(query.value("content_type").toInt());msg.content.data = query.value("content_data").toString();msg.content.mimeType = query.value("content_mime_type").toString();msg.content.fid = query.value("content_fid").toString();return msg;
}bool DataBase::createConversationTable()
{QSqlQuery query (_db);QString sql_str = R"(CREATE TABLE IF NOT EXISTS conversations(id          INTEGER PRIMARY KEY AUTOINCREMENT,uid         TEXT    NOT NULL UNIQUE,to_uid      INTEGER NOT NULL,from_uid    INTEGER NOT NULL,create_time TEXT ,update_time TEXT ,name        TEXT ,icon        TEXT ,status      INTEGER DEFAULT 0,deleted     INTEGER DEFAULT 0,pined       INTEGER DEFAULT 0,processed   INTEGER DEFAULT 0,env         INTEGER DEFAULT 0))";if (!query.exec(sql_str)){qDebug() << "Failed to create table:" << query.lastError().text();return false;}// 创建索引QStringList indexes = {"CREATE INDEX IF NOT EXISTS idx_from_uid ON conversations(from_uid)","CREATE INDEX IF NOT EXISTS idx_to_uid ON conversations(to_uid)","CREATE INDEX IF NOT EXISTS idx_deleted ON conversations(deleted)","CREATE INDEX IF NOT EXISTS idx_pined ON conversations(pined)","CREATE INDEX IF NOT EXISTS idx_processed ON conversations(processed)",};for (const QString& sql : indexes) {if (!QSqlQuery(_db).exec(sql)) {qDebug() << "Failed to create index:" << _db.lastError().text();}}return true;
}bool DataBase::createOrUpdateConversation(const ConversationItem& conv)
{QSqlQuery query(_db);query.prepare(R"(INSERT OR REPLACE INTO conversations(uid,to_uid, from_uid, create_time, update_time, name, icon,status,deleted,pined,processed,env)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?))");qint64 now = QDateTime::currentSecsSinceEpoch();query.addBindValue(conv.id);query.addBindValue(conv.to_uid);query.addBindValue(conv.from_uid);query.addBindValue(conv.create_time.toString("yyyy-MM-dd HH:mm:ss"));query.addBindValue(!conv.update_time.isNull() ? conv.update_time.toString("yyyy-MM-dd HH:mm:ss") : QString::number(now));query.addBindValue(conv.name);query.addBindValue(conv.icon);query.addBindValue(conv.status);query.addBindValue(conv.deleted);query.addBindValue(conv.pined);query.addBindValue(conv.processed?1:0);query.addBindValue(conv.env);if (!query.exec()) {qDebug() << "Failed to create/update conversation:" << query.lastError().text();return false;}return true;
}bool DataBase::existConversation(int peerUid)
{QSqlQuery query(_db);query.prepare(R"(SELECT COUNT(*) FROM conversationsWHERE to_uid = ?AND deleted = 0)");query.addBindValue(peerUid);if (!query.exec()){qDebug() << "Failed to check conversation existence:" << query.lastError().text();return false;}if (query.next()){int count = query.value(0).toInt();return count > 0;}return false;
}bool DataBase::createOrUpdateConversations(const std::vector<ConversationItem> &conversations)
{if (conversations.empty()) {return true;}_db.transaction();QSqlQuery query(_db);query.prepare(R"(INSERT OR REPLACE INTO conversations(uid, to_uid, from_uid, create_time, update_time, name, icon, status, delted, pined,processed)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?))");QDateTime now = QDateTime::currentDateTime();// 预先绑定所有参数QVariantList ids, to_uids, from_uids, create_times, update_times;QVariantList names, icons, statuses, deleteds, pineds,processeds,envs;for (const auto& conv : conversations) {ids                 << conv.id;to_uids             << conv.to_uid;from_uids           << conv.from_uid;create_times        << conv.create_time.toString("yyyy-MM-dd HH:mm:ss");update_times        << (!conv.update_time.isNull() ? conv.update_time.toString("yyyy-MM-dd HH:mm:ss") : now.toString("yyyy-MM-dd HH:mm:ss"));names               << conv.name;icons               << conv.icon;statuses            << conv.status;deleteds            << conv.deleted;pineds              << conv.pined;processeds          << (conv.processed ? 1 : 0);envs                << conv.env;}qDebug() << "conversations size : " << conversations.size();query.addBindValue(ids);query.addBindValue(to_uids);query.addBindValue(from_uids);query.addBindValue(create_times);query.addBindValue(update_times);query.addBindValue(names);query.addBindValue(icons);query.addBindValue(statuses);query.addBindValue(deleteds);query.addBindValue(pineds);query.addBindValue(processeds);query.addBindValue(envs);if (!query.execBatch()) {qDebug() << "Failed to batch create/update conversations:" << query.lastError().text();_db.rollback();return false;}if (!_db.commit()) {qDebug() << "Failed to commit transaction:" << _db.lastError().text();_db.rollback();return false;}qDebug() << "Successfully batch created/updated" << conversations.size() << "conversations";return true;
}bool DataBase::createOrUpdateConversations(const std::vector<std::shared_ptr<ConversationItem>> &conversations)
{if (conversations.empty()) {return true;}qDebug() << "createOrUpdateConversations";_db.transaction();QSqlQuery query(_db);query.prepare(R"(INSERT OR REPLACE INTO conversations(uid, to_uid, from_uid, create_time, update_time, name, icon, status, delted, pined,processed,env)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?))");QDateTime now = QDateTime::currentDateTime();// 预先绑定所有参数QVariantList ids, to_uids, from_uids, create_times, update_times;QVariantList names, icons, statuses, deleteds, pineds,processeds,envs;for (const auto& conv_ptr : conversations) {const ConversationItem& conv = *conv_ptr;  // 解引用 shared_ptrids                 << conv.id;to_uids             << conv.to_uid;from_uids           << conv.from_uid;create_times        << conv.create_time;update_times        << (!conv.update_time.isNull() ? conv.update_time.toString("yyyy-MM-dd HH:mm:ss") : now.toString("yyyy-MM-dd HH:mm:ss"));names               << conv.name;icons               << conv.icon;statuses            << conv.status;deleteds            << conv.deleted;pineds              << conv.pined;processeds          << (conv.processed?1:0);envs                << conv.env;}query.addBindValue(ids);query.addBindValue(to_uids);query.addBindValue(from_uids);query.addBindValue(create_times);query.addBindValue(update_times);query.addBindValue(names);query.addBindValue(icons);query.addBindValue(statuses);query.addBindValue(deleteds);query.addBindValue(pineds);query.addBindValue(processeds);query.addBindValue(envs);if (!query.execBatch()) {qDebug() << "Failed to batch create/update conversations (shared_ptr version):" << query.lastError().text();_db.rollback();return false;}if (!_db.commit()) {qDebug() << "Failed to commit transaction:" << _db.lastError().text();_db.rollback();return false;}qDebug() << "Successfully batch created/updated" << conversations.size() << "conversations (shared_ptr version)";return true;
}
std::vector<ConversationItem> DataBase::getConversationList()
{std::vector<ConversationItem>conversations;QSqlQuery query(_db);query.prepare(R"(SELECT * FROM conversationsWHERE deleted = 0 AND from_uid = ?ORDER BY pined desc , update_time desc)");query.addBindValue(UserManager::GetInstance()->GetUid());if (!query.exec()){qDebug() << "Failed to get conversations list:" << query.lastError().text();return conversations;}while (query.next()) {ConversationItem conv = createConversationFromQuery(query);// 补充动态数据conv.message = getLastMessage(conv.to_uid);conversations.push_back(std::move(conv));}return conversations;
}std::vector<std::shared_ptr<ConversationItem> > DataBase::getConversationListPtr()
{std::vector<std::shared_ptr<ConversationItem> >conversations;QSqlQuery query(_db);query.prepare(R"(SELECT * FROM conversationsWHERE deleted = 0 AND from_uid = ?ORDER BY pined desc , update_time desc)");qDebug() << "getUid:"<<UserManager::GetInstance()->GetUid();query.addBindValue(UserManager::GetInstance()->GetUid());if (!query.exec()){qDebug() << "Failed to get conversations list:" << query.lastError().text();return conversations;}while (query.next()) {ConversationItem conv = createConversationFromQuery(query);// 补充动态数据conv.message = getLastMessage(conv.to_uid);conversations.push_back(std::make_shared<ConversationItem>(std::move(conv)));}return conversations;
}ConversationItem DataBase::getConversation(int peerUid)
{ConversationItem conv;QSqlQuery query(_db);query.prepare(R"(SELECT * FROM conversationsWHERE to_uid = ?AND from_uid = ?)");query.addBindValue(peerUid);query.addBindValue(UserManager::GetInstance()->GetUid());if (!query.exec() || !query.next()){qDebug() << "Failed to get conversation :" << query.lastError().text();return conv;}conv = createConversationFromQuery(query);return conv;
}ConversationItem DataBase::createConversationFromQuery(const QSqlQuery &query)
{// 添加有效性检查if (!query.isValid()) {qDebug() << "Warning: createConversationFromQuery called with invalid query";return ConversationItem();}ConversationItem conv;conv.id = query.value("uid").toString();conv.from_uid       =     query.value("from_uid").toInt();conv.to_uid         =     query.value("to_uid").toInt();conv.create_time    =     query.value("create_time").toDateTime();conv.update_time    =     query.value("update_time").toDateTime();conv.name           =     query.value("name").toString();conv.icon           =     query.value("icon").toString();conv.status         =     query.value("status").toInt();conv.deleted        =     query.value("deleted").toInt();conv.pined          =     query.value("pined").toInt();conv.processed      =     query.value("processed").toInt() == 1 ? true: false;conv.env            =     query.value("env").toInt();return conv;
}QString DataBase::getLastMessage(int peerUid)
{QString text;;int myUid = UserManager::GetInstance()->GetUid();QSqlQuery query(_db);query.prepare(R"(SELECT * FROM messagesWHERE (from_uid = ? AND to_uid = ?) OR (from_uid = ? AND to_uid = ?)ORDER BY timestamp descLIMIT 1)");query.addBindValue(myUid);query.addBindValue(peerUid);query.addBindValue(peerUid);query.addBindValue(myUid);if (query.exec() && query.next()) {// 在访问任何值之前,先检查查询是否在有效记录上if (!query.isValid()) {qDebug() << "Query is not valid in getLastMessage";return "";}switch(query.value("content_type").toInt()){case static_cast<int>(MessageType::AudioMessage):text = "[Audio]";break;case static_cast<int>(MessageType::TextMessage):text = query.value("content_data").toString();break;case static_cast<int>(MessageType::ImageMessage):text = "[Image]";break;case static_cast<int>(MessageType::VideoMessage):text = "[Video]";break;case static_cast<int>(MessageType::OtherFileMessage):text = "[File]";break;default:text = "";break;};}return text;
}bool DataBase::createFriendsTable()
{QSqlQuery query(_db);QString sql_str ="CREATE TABLE IF NOT EXISTS friends (""id          INTEGER PRIMARY KEY,""from_uid    INTEGER NOT NULL,""to_uid      INTEGER NOT NULL,""sex         INTEGER NOT NULL DEFAULT 0,""status      INTEGER NOT NULL DEFAULT 0,""email       TEXT,""name        TEXT    NOT NULL,""avatar      TEXT,""desc        TEXT,""back        TEXT"   // 备用字段")";if (!query.exec(sql_str)){qDebug() << "Failed to create friends table:" << query.lastError().text();return false;}QStringList indexes = {"CREATE INDEX IF NOT EXISTS idx_friends_from_uid ON friends(from_uid)","CREATE INDEX IF NOT EXISTS idx_friends_to_uid ON friends(to_uid)","CREATE INDEX IF NOT EXISTS idx_friends_status ON friends(status)","CREATE INDEX IF NOT EXISTS idx_friends_name ON friends(name)"};for (const QString& sql : indexes) {if (!QSqlQuery(_db).exec(sql)) {qDebug() << "Failed to create friends index:" << _db.lastError().text();}}return true;
}std::shared_ptr<UserInfo> DataBase::getFriendInfoPtr(int peerUid)
{qDebug() << "peerUid:" << peerUid;QSqlQuery query(_db);query.prepare(R"(SELECT * FROM friendsWHERE to_uid = ?AND from_uid = ?)");query.addBindValue(UserManager::GetInstance()->GetPeerUid());query.addBindValue(UserManager::GetInstance()->GetUid());if (!query.exec() || !query.next()){qDebug()<< "Failed to get FriendInfo" << query.lastError().text();return std::shared_ptr<UserInfo>();}std::shared_ptr<UserInfo> info = std::make_shared<UserInfo>(createFriendInfoFromQuery(query));return info;
}UserInfo DataBase::getFriendInfo(int peerUid)
{QSqlQuery query(_db);query.prepare(R"(SELECT * FROM friendsWHERE to_uid = ?)");query.addBindValue(peerUid);if (!query.exec() || !query.next()){qDebug()<< "Failed to get FriendInfo" << query.lastError().text();return UserInfo{};}UserInfo info = createFriendInfoFromQuery(query);return info;
}std::vector<UserInfo> DataBase::getFriends()
{std::vector<UserInfo>friends;QSqlQuery query(_db);query.prepare(R"(SELECT * FROM friendsWHERE from_uid = ?)");query.addBindValue(UserManager::GetInstance()->GetUid());if (!query.exec() || !query.next()){qDebug() << "Failed to get Friends list:" << query.lastError().text();return friends;}while (query.next()) {UserInfo info = createFriendInfoFromQuery(query);friends.push_back(info);}return friends;
}std::vector<std::shared_ptr<UserInfo>> DataBase::getFriendsPtr()
{std::vector<std::shared_ptr<UserInfo> >friends;QSqlQuery query(_db);query.prepare(R"(SELECT * FROM friendsWHERE from_uid = ?)");query.addBindValue(UserManager::GetInstance()->GetUid());if (!query.exec() || !query.next()){qDebug() << "Failed to get Friends list:" << query.lastError().text();return friends;}while (query.next()) {UserInfo info = createFriendInfoFromQuery(query);friends.push_back(std::make_shared<UserInfo>(std::move(info)));}return friends;
}bool DataBase::storeFriends(const std::vector<std::shared_ptr<UserInfo>> friends)
{if (friends.empty()){return false;}if (!_db.transaction()){qDebug() << "Transaction Start Error : " << _db.lastError().text();return false;}QSqlQuery query(_db);query.prepare(R"(INSERT OR REPLACE INTO friends(from_uid, to_uid, sex, status, email, name, avatar, desc, back)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?))");// 使用 QVariantList 来批量绑定QVariantList from_uids, to_uids, sexes, statuses, emails, names, avatars, descs, backs;for (const auto& friend_ptr : friends){const UserInfo& info = *friend_ptr;from_uids << UserManager::GetInstance()->GetUid();to_uids << info.id;sexes << info.sex;statuses << info.status;emails << info.email;names << info.name;avatars << info.avatar;descs << info.desc;backs << info.back;}query.addBindValue(from_uids);query.addBindValue(to_uids);query.addBindValue(sexes);query.addBindValue(statuses);query.addBindValue(emails);query.addBindValue(names);query.addBindValue(avatars);query.addBindValue(descs);query.addBindValue(backs);if (!query.execBatch()){qDebug() << "ExecBatch Error (shared_ptr friends):" << query.lastError().text();_db.rollback();return false;}if (!_db.commit()){qDebug() << "Commit Error:" << _db.lastError().text();_db.rollback();return false;}qDebug() << "Successfully stored" << friends.size() << "friends (shared_ptr version)";return true;
}// bool DataBase::storeFriends(const std::vector<std::shared_ptr<UserInfo> > friends)
// {
//     if (friends.empty()){
//         return false;
//     }
//     if (!_db.transaction()){
//         qDebug() << "Transaction Start Error : " << _db.lastError().text();
//         return false;
//     }//     QSqlQuery query(_db);
//     query.prepare(R"(
//         INSERT OR REPLACE INTO friends
//         (from_uid, to_uid, sex, status, email, name, avatar, desc, back)
//         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
//     )");//     for (const auto& friend_ptr : friends){
//         const UserInfo& info = *friend_ptr;
//         query.addBindValue(UserManager::GetInstance()->GetUid());
//         query.addBindValue(info.id);
//         query.addBindValue(info.sex);
//         query.addBindValue(info.status);
//         query.addBindValue(info.email);
//         query.addBindValue(info.name);
//         query.addBindValue(info.avatar);
//         query.addBindValue(info.desc);  // 映射到 description 字段
//         query.addBindValue(info.back);
//     }//     if (!query.execBatch()){
//         qDebug() << "ExecBatch Error (shared_ptr friends):" << query.lastError().text();
//         _db.rollback();
//         return false;
//     }//     if (!_db.commit()){
//         qDebug() << "Commit Error:" << _db.lastError().text();
//         _db.rollback();
//         return false;
//     }//     qDebug() << "Successfully stored" << friends.size() << "friends (shared_ptr version)";
//     return true;
// }bool DataBase::storeFriends(const std::vector<UserInfo> friends)
{if (friends.empty()){return false;}if (!_db.transaction()){qDebug() << "Transaction Start Error : " << _db.lastError().text();return false;}QSqlQuery query(_db);query.prepare(R"(INSERT OR REPLACE INTO friends(from_uid, to_uid, sex, status, email, name, avatar, desc, back)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?))");for (const auto& friend_ptr : friends){const UserInfo& info = friend_ptr;query.addBindValue(UserManager::GetInstance()->GetUid());query.addBindValue(info.id);query.addBindValue(info.sex);query.addBindValue(info.status);query.addBindValue(info.email);query.addBindValue(info.name);query.addBindValue(info.avatar);query.addBindValue(info.desc);  // 映射到 description 字段query.addBindValue(info.back);}if (!query.execBatch()){qDebug() << "ExecBatch Error (shared_ptr friends):" << query.lastError().text();_db.rollback();return false;}if (!_db.commit()){qDebug() << "Commit Error:" << _db.lastError().text();_db.rollback();return false;}qDebug() << "Successfully stored" << friends.size() << "friends (shared_ptr version)";return true;
}bool DataBase::storeFriend(const UserInfo &info)
{QSqlQuery query(_db);query.prepare(R"(INSERT OR REPLACE INTO friends(from_uid, to_uid, sex, status, email, name, avatar, desc, back)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?))");query.addBindValue(UserManager::GetInstance()->GetUid());query.addBindValue(info.id);query.addBindValue(info.sex);query.addBindValue(info.status);query.addBindValue(info.email);query.addBindValue(info.name);query.addBindValue(info.avatar);query.addBindValue(info.desc);  // 映射到 description 字段query.addBindValue(info.back);if (!query.exec()){qDebug() << "Failed to store friend:" << query.lastError().text();return false;}qDebug() << "Successfully stored friend:" << info.name;return true;
}bool DataBase::storeFriend(const std::shared_ptr<UserInfo> &info)
{QSqlQuery query(_db);query.prepare(R"(INSERT OR REPLACE INTO friends(from_uid, to_uid, sex, status, email, name, avatar, desc, back)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?))");query.addBindValue(UserManager::GetInstance()->GetUid());query.addBindValue(info->id);query.addBindValue(info->sex);query.addBindValue(info->status);query.addBindValue(info->email);query.addBindValue(info->name);query.addBindValue(info->avatar);query.addBindValue(info->desc);  // 映射到 description 字段query.addBindValue(info->back);if (!query.exec()){qDebug() << "Failed to store friend:" << query.lastError().text();return false;}qDebug() << "Successfully stored friend:" << info->name;return true;
}UserInfo DataBase::createFriendInfoFromQuery(const QSqlQuery &query)
{UserInfo info;info.id = query.value("to_uid").toInt();info.name = query.value("name").toString();info.avatar = query.value("avatar").toString();info.sex = query.value("sex").toInt();info.desc = query.value("desc").toString();return info;
}

TcpManager

这是信息接受的大门新增了很多的处理,包括信号,回调函数等。

首先是信号

	void on_change_friend_status(int,int);  // to FriendsListPart::do_change_friend_status;void on_change_chat_history(std::vector<std::shared_ptr<MessageItem>>); // to ChatArea::do_change_chat_history;void on_get_message(const MessageItem&);// to MessageListPart::do_get_messagevoid on_get_messages(const std::vector<std::shared_ptr<MessageItem>>&lists);    // to MessageListPart::do_get_messages

包括处理好友状态(加不加红点等),获取单个消息,获取消息列表等。

接下来是各种处理

ID_CHAT_LOGIN_RSP

新增加了获取绘画列表和未读消息列表的处理

//TODO: 会话列表if (jsonObj.contains("conversations")){const QJsonArray &conversations = jsonObj["conversations"].toArray();qDebug() << "conversations" <<conversations;std::vector<std::shared_ptr<ConversationItem>>lists;for(const QJsonValue&value:conversations){QJsonObject obj = value.toObject();auto conversation = std::make_shared<ConversationItem>();// 解析字段,仿照好友列表的写法conversation->id = obj["uid"].toString();conversation->to_uid = obj["to_uid"].toInt();conversation->from_uid = obj["from_uid"].toInt();conversation->create_time = obj["create_time"].toVariant().toDateTime();conversation->update_time = obj["update_time"].toVariant().toDateTime();conversation->name = obj["name"].toString();conversation->icon = obj["icon"].toString();conversation->status = obj["status"].toInt();conversation->deleted = obj["deleted"].toInt();conversation->pined = obj["pined"].toInt();// UserManager::GetInstance()->GetMessages().push_back(conversation);lists.push_back(conversation);}if (UserManager::GetInstance()->GetMessages().size()<lists.size()){(void)std::async(std::launch::async,[this,&lists](){DataBase::GetInstance().createOrUpdateConversations(lists);UserManager::GetInstance()->GetMessages() = std::move(lists);UserManager::GetInstance()->ResetLoadMessages();emit on_add_messages_to_list(UserManager::GetInstance()->GetMessagesPerPage());});}}// 解析消息列表if (jsonObj.contains("unread_messages")){const QJsonArray&unread_messages = jsonObj["unread_messages"].toArray();std::vector<std::shared_ptr<MessageItem>>lists;for (const QJsonValue&value:unread_messages){QJsonObject obj = value.toObject();auto message = std::make_shared<MessageItem>();message->id = obj.value("id").toVariant().toString();message->from_id = obj.value("from_id").toInt();message->to_id = obj.value("to_id").toInt();message->timestamp =QDateTime::fromString( obj.value("timestamp").toString());message->env = MessageEnv(obj.value("env").toInt());message->content.type = MessageType(obj.value("content_type").toInt());message->content.data = obj.value("content_data").toString();message->content.mimeType = obj.value("content_mime_type").toString();message->content.fid = obj.value("content_fid").toString();lists.push_back(message);}emit on_get_messages(lists);}

同时在回调的开始,进行了如下操作:

 // 初始化本地数据库
DataBase::GetInstance().initialization();// 基本信息
UserManager::GetInstance()->SetName(jsonObj["name"].toString());
UserManager::GetInstance()->SetEmail(jsonObj["email"].toString());
UserManager::GetInstance()->SetToken(jsonObj["token"].toString());
UserManager::GetInstance()->SetIcon(jsonObj["icon"].toString());
UserManager::GetInstance()->SetUid(jsonObj["uid"].toInt());
UserManager::GetInstance()->SetSex(jsonObj["sex"].toInt());
UserManager::GetInstance()->SetStatus(1);// 先是本地加载
UserManager::GetInstance()->GetFriends() = DataBase::GetInstance().getFriendsPtr();
UserManager::GetInstance()->GetMessages() = DataBase::GetInstance().getConversationListPtr();emit on_add_friends_to_list(UserManager::GetInstance()->GetFriendsPerPage());
emit on_add_messages_to_list(UserManager::GetInstance()->GetMessagesPerPage());

没错,初始化数据库和本地信息,先把本地数据库的内容加载到程序中。之后解析的时候,仍然会得到来自服务器的好友列表会话列表等,通过比较,进行同步:

if (lists.size()>UserManager::GetInstance()->GetFriends().size()){(void)std::async(std::launch::async,[this,&lists](){qDebug() << "friends";DataBase::GetInstance().storeFriends(lists);UserManager::GetInstance()->GetFriends() = std::move(lists);UserManager::GetInstance()->ResetLoadFriends();emit on_add_friends_to_list(UserManager::GetInstance()->GetFriendsPerPage());});
}if (UserManager::GetInstance()->GetMessages().size()<lists.size()){(void)std::async(std::launch::async,[this,&lists](){DataBase::GetInstance().createOrUpdateConversations(lists);UserManager::GetInstance()->GetMessages() = std::move(lists);UserManager::GetInstance()->ResetLoadMessages();emit on_add_messages_to_list(UserManager::GetInstance()->GetMessagesPerPage());});
}

ID_NOTIFY_TEXT_CHAT_MSG_REQ

解析单个消息的回调函数

/*** @brief 收到消息*/
_handlers[RequestType::ID_NOTIFY_TEXT_CHAT_MSG_REQ] = [this](RequestType requestType,int len,QByteArray data){im::MessageItem pb;if (pb.ParseFromString(data.toStdString())){emit on_get_message(fromPb(pb));}else{qDebug() << "Failed to parse Message from data";}
};

ID_TEXT_CHAT_MSG_RSP

发送消息后的回包,我们这里暂未处理,按道理应该有的,发送完确认是否发送成功。比如以后发送大文件,需要同步进度什么的。

_handlers[RequestType::ID_TEXT_CHAT_MSG_RSP] = [this](RequestType requestType,int len,QByteArray data){// qDebug() << "暂时不处理";};

ID_GET_MESSAGES_OF_FRIEND_RSP

获取与某好友的消息列表,用户消息漫游。

_handlers[RequestType::ID_GET_MESSAGES_OF_FRIEND_RSP] = [this](RequestType requestType,int len,QByteArray data){// TODO:获取与某人的聊天记录列表};

QTcpSocket::errorOccurred

当连接服务器失败的时候,仍然要加载本地的数据,仍可以看到过往的信息。

// 错误connect(&_socket,&QTcpSocket::errorOccurred,[&](QTcpSocket::SocketError socketError){qDebug() << "Socket Error["<<socketError<< "]:" << _socket.errorString();emit on_login_failed(static_cast<int>(ErrorCodes::ERROR_NETWORK));// 初始化本地数据库DataBase::GetInstance().initialization();// 连接网络失败直接使用本地数据库展示。UserManager::GetInstance()->GetFriends() = DataBase::GetInstance().getFriendsPtr();UserManager::GetInstance()->GetMessages() = DataBase::GetInstance().getConversationListPtr();emit on_add_friends_to_list(UserManager::GetInstance()->GetFriendsPerPage());emit on_add_messages_to_list(UserManager::GetInstance()->GetMessagesPerPage());});

MessageItem/im::MessageItem

在之前由于过于设计,想着每次消息的多元类型,强制每次消息必须发送一个QList列表,可以包含多个MessageItem,但实际上很多余,也不实用。至少大腕微信不是这样做的。

因此我们摒弃了冗余的列表,每次就是发一种类型的消息,使用了更轻便的MessageItem

#ifndef MESSAGETYPES_H
#define MESSAGETYPES_H#include <QObject>
#include <QString>
#include <QDateTime>
#include <QUrl>
#include <QVariant>
#include <QUuid>
#include "../../../../usermanager.h"
#include "../../../../proto/im.pb.h"enum class MessageType{TextMessage,ImageMessage,VideoMessage,AudioMessage,OtherFileMessage
};enum class MessageSource{Me = 0,Peer,   // 单独聊天
};enum class MessageEnv{Private,    // 0Group       // 1
};struct MessageContent{MessageType     type;           // 自定义类型QVariant        data;           // 如果是文本文件,存放在这里,如果是二进制,此为空。QString         mimeType;       // 具体的类型比如text/plainQString         fid;            // 文件服务器需要用
};struct MessageItem{QString               id;           // 唯一的消息idint                   to_id;        // 接受者idint                   from_id;      // 发送者的idQDateTime             timestamp;    // 时间MessageEnv            env;          // 私聊还是群聊MessageContent        content;      // 实际的内容串bool                  isSelected;   // 之后可能会有聊天记录的选择,删除int                   status;MessageItem():id(QUuid::createUuid().toString()),from_id(UserManager::GetInstance()->GetUid()),timestamp(QDateTime::currentDateTime()),env(MessageEnv::Private),isSelected(false),status(0){}
};// 转成发给服务器的im::MessageItem
static im::MessageItem toPb(const MessageItem &m)
{im::MessageItem pb;pb.set_id(m.id.toStdString());pb.set_from_id(m.from_id);pb.set_to_id(m.to_id);pb.set_timestamp(m.timestamp.toString("yyyy-MM-dd HH:mm:ss").toStdString());pb.set_env(static_cast<int32_t>(m.env));qDebug() << "env!!!:" << static_cast<int>(m.env);auto* c = pb.mutable_content();c->set_type(static_cast<int32_t>(m.content.type));c->set_data(m.content.data.toString().toStdString());c->set_mime_type(m.content.mimeType.toStdString());c->set_fid(m.content.fid.toStdString());return pb;
}// 服务器收回来解析成MessageItem
static MessageItem fromPb(const im::MessageItem&pb)
{QString format = "yyyy-MM-dd HH:mm:ss";MessageItem m;m.id                = QString::fromStdString(pb.id());m.to_id             = pb.to_id();m.from_id           = pb.from_id();m.timestamp         = QDateTime::fromString(QString::fromStdString(pb.timestamp()),format);m.env               = MessageEnv(pb.env());m.content.fid       = QString::fromStdString(pb.content().fid());m.content.type      = MessageType(pb.content().type());m.content.data      = QString::fromStdString(pb.content().data());m.content.mimeType  = QString::fromStdString(pb.content().mime_type());return m;
}
/*id          INTEGER PRIMARY KEY AUTOINCREMENT,to_uid      INTEGER NOT NULL UNIQUE,from_uid    INTEGER NOT NULL,create_time INTEGER NOT NULL,update_time INTEGER NOT NULL,name        TEXT    NOT NULL,icon        TEXT    NOT NULL
*/struct ConversationItem
{QString               id;           // 唯一idint                   from_uid;     // 自己int                   to_uid;       // 对方idQDateTime             create_time;  // 创建时间QDateTime             update_time;  // 更新时间QString               name;         // 名称QString               icon;         // 头像int                   status;       // 在线状态int                   deleted;      // 是否删除int                   pined;        // 是否置顶QString               message;      // 最近消息bool                  processed;    // 是否处理了int                   env;          // 0私聊,1群聊ConversationItem(): id (QUuid::createUuid().toString()), status(0), deleted(0), pined(0), processed(true), env(0){}};Q_DECLARE_METATYPE(MessageContent)
Q_DECLARE_METATYPE(QList<MessageContent>)#endif // MESSAGETYPES_H

但也正如在后端哪里提到过的,发送信息使用了protobuf,原因是考虑到安全性和效率。使用protobuf,效率更高,速度更快,更轻量,同时二进制的形式也不容易直接被解析,也要比json更安全。

InputArea

在这里是发送消息的重要区域。在MessageItem中的content.fid实际就是为之后使用文件服务器的编号,目前主要是普通的文本信息,因此暂不使用。

下面是解析文本消息的函数

std::optional<QList<MessageItem>> InputArea::parseMessageContent()
{QTextDocument* doc = m_textEdit->document();QTextBlock currentBlock = doc->begin();QList<MessageItem> item_list;int peerUid = UserManager::GetInstance()->GetPeerUid();if (peerUid < 0){return std::nullopt;}// 用于累积文本内容QString accumulatedText;while (currentBlock.isValid()) {QTextBlock::Iterator it;for (it = currentBlock.begin(); !(it.atEnd()); ++it) {QTextFragment fragment = it.fragment();if (fragment.isValid()) {QTextCharFormat format = fragment.charFormat();if (format.isImageFormat()) {// 如果遇到图片,先处理累积的文本(如果有的话)if (!accumulatedText.trimmed().isEmpty()) {MessageItem textItem;textItem.timestamp = QDateTime::currentDateTime();textItem.env = UserManager::GetInstance()->GetEnv();textItem.from_id = UserManager::GetInstance()->GetUid();textItem.to_id = peerUid;textItem.content.mimeType = "text/plain";textItem.content.type = MessageType::TextMessage;textItem.content.data = accumulatedText;textItem.status = 0;item_list.append(textItem);accumulatedText.clear(); // 清空累积的文本}// 处理图片QTextImageFormat imageFormat = format.toImageFormat();QString imagePath = imageFormat.name();QMimeDatabase db;QMimeType mime = db.mimeTypeForFile(imagePath);MessageItem imageItem;imageItem.env = UserManager::GetInstance()->GetEnv();imageItem.from_id = UserManager::GetInstance()->GetUid();imageItem.to_id = peerUid;imageItem.status = 0;imageItem.content.mimeType = mime.name();imageItem.content.type = MessageType::ImageMessage;imageItem.content.data = imagePath;item_list.append(imageItem);} else {// 累积文本内容QString text = fragment.text();accumulatedText += text;}}}// 块结束时添加换行符(如果需要保持段落结构)if (currentBlock.next().isValid()) {accumulatedText += "\n";}currentBlock = currentBlock.next();}// 处理最后累积的文本(如果有的话)if (!accumulatedText.trimmed().isEmpty()) {MessageItem textItem;textItem.env = UserManager::GetInstance()->GetEnv();textItem.from_id = UserManager::GetInstance()->GetUid();textItem.to_id = peerUid;textItem.status = 0;textItem.content.mimeType = "text/plain";textItem.content.type = MessageType::TextMessage;textItem.content.data = accumulatedText;item_list.append(textItem);}return item_list.size() == 0 ? std::nullopt : std::make_optional(item_list);
}

点击发送的时候,首先parseMessageContent进行解析出一个多多个消息类型MessageItem装入list,之后由函数do_send_clicked进行调用TcpManager函数发送

void InputArea::do_send_clicked()
{std::optional<QList<MessageItem>> list = parseMessageContent();qDebug() << list.has_value();if (list.has_value()) {for(const auto&item:list.value()){if (item.content.type == MessageType::TextMessage){if (item.content.data.toString().size() > 2048){QMessageBox msgBox;msgBox.setWindowTitle("Too Long Text!");msgBox.setText("Too Long Text!");msgBox.setIcon(QMessageBox::Warning);msgBox.setStandardButtons(QMessageBox::Ok);// macOS 风格样式表msgBox.setStyleSheet(R"(QMessageBox {background-color: #f5f5f7;border: 1px solid #d0d0d0;border-radius: 10px;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;}QMessageBox QLabel {color: #1d1d1f;font-size: 14px;font-weight: 400;padding: 15px;}QMessageBox QLabel#qt_msgbox_label {min-width: 300px;}QMessageBox QPushButton {background-color: #007aff;color: white;border: none;border-radius: 6px;padding: 8px 24px;font-size: 13px;font-weight: 500;min-width: 80px;margin: 5px;}QMessageBox QPushButton:hover {background-color: #0056d6;}QMessageBox QPushButton:pressed {background-color: #0040a8;}QMessageBox QPushButton:focus {outline: 2px solid #007aff;outline-offset: 2px;})");msgBox.exec();}else{emit on_message_sent(item);clear();}}else{emit on_message_sent(item);clear();}}}
}

接受消息流程

当对方发来消息的时候,实际上需要考虑的很多,比如当前是否有与对方的会话?当前会话是否是与发来消息好友的人的会话?需不需要给回家加上红点?还要修改会话预览的内容。

点击会话之后,消息的交互区域还需要本地数据库查询,更新消息,两个不相干的区域也要有信号和槽的交互,但是双方不可能互有对方的指针,即使是依赖注入也很是麻烦,那就还需要一个SignalRouter信号中转器。如图所示:

image-20251126153216819

void MessagesListPart::do_get_message(const MessageItem &message)
{int peerUid = message.from_id;qDebug() << "Received message from:" << peerUid;// 先直接存储到数据库if (messagesModel->existMessage(peerUid)){// 更新最近消息messagesModel->setData(messagesModel->indexFromUid(peerUid), message.content.data, MessagesModel::MessageRole);if (messagesModel->indexFromUid(peerUid) == messagesList->currentIndex()){// 添加messageemit SignalRouter::GetInstance().on_add_new_message(message);auto p = message;p.status = 1;DataBase::GetInstance().storeMessage(p);}else{// 红点do_change_message_status(peerUid, false);DataBase::GetInstance().storeMessage(message);UserManager::GetInstance()->setHistoryTimestamp(peerUid,QDateTime::currentDateTime());}}else{DataBase::GetInstance().storeMessage(message);// 不存在会话,那就创建插入会话UserManager::GetInstance()->SetPeerUid(message.from_id);UserManager::GetInstance()->SetEnv(MessageEnv(message.env));std::shared_ptr<UserInfo> info = DataBase::GetInstance().getFriendInfoPtr(peerUid);ConversationItem conv;// 这里故意反过来,对于自己来说所有的好友都是to,自己是fromconv.to_uid = message.from_id;conv.from_uid = message.to_id;conv.icon = info ? info->avatar : "";if (message.content.type == MessageType::TextMessage){conv.message = message.content.data.toString();}else{//TODO:比如图片,文件。。。conv.message = "Other:暂时没做";}conv.pined = 0;conv.status = 1;conv.create_time = QDateTime::currentDateTime();conv.update_time = QDateTime::currentDateTime();conv.deleted = 0;conv.name = info->name;qDebug() <<"info->name:" <<info->name;conv.processed = false;conv.env = static_cast<int>(UserManager::GetInstance()->GetEnv());messagesModel->addPreMessage(conv);UserManager::GetInstance()->setHistoryTimestamp(conv.from_uid, QDateTime::currentDateTime());// 存放数据库中DataBase::GetInstance().createOrUpdateConversation(conv);UserManager::GetInstance()->GetMessages().push_back(std::make_shared<ConversationItem>(std::move(conv)));_wait_sync_conversations.push_back(std::move(conv));if (_wait_sync_conversations.size() >= 10){syncConversations();}}// 刷新视图messagesList->update();messagesList->viewport()->update();
}

含义直白,很好理解,但是有一个很重要的地方。那就是我们设置了setHistoryTimestamp这个函数,而这个函数是我们实现消息分段加载的关键。

我们首先看数据库消息获取的的函数getMessages

std::vector<MessageItem> DataBase::getMessages(int peerUid,  QString sinceTimestamp,int limit)
{std::vector<MessageItem>messages;QSqlQuery query(_db);QString sql = R"(SELECT * FROM messagesWHERE ((from_uid = ? AND to_uid = ?) OR (from_uid = ? AND to_uid = ?)) AND owner = ?)";QVariantList params;params << peerUid << UserManager::GetInstance()->GetUid() << UserManager::GetInstance()->GetUid() << peerUid << UserManager::GetInstance()->GetUid();if (!sinceTimestamp.isEmpty()){sql += "AND timestamp < ? ";params << sinceTimestamp;}sql += "ORDER BY timestamp desc ";if (limit > 0){sql+="LIMIT ?";params << limit;}query.prepare(sql);for(int i = 0;i<params.size();++i){query.addBindValue(params[i]);}if (!query.exec()){qDebug() << "Failed To Get Messages:" << query.lastError().text();return messages;}int count = 0;QDateTime last_time;while(query.next()){messages.push_back(createMessageFromQuery(query));count++;last_time = query.value("timestamp").toDateTime();}if (count > 0){emit SignalRouter::GetInstance().on_change_last_time(peerUid,last_time);}if (count<limit){UserManager::GetInstance()->setMessagesFinished(peerUid);}return messages;
}

image-20251126153913967

假如我们有6条消息,但是我们分段加载,目前仅仅加载前3条,根据我们目前的策略,第一次查询,timestamp应该<currentTimestamp,同时查询结果DESC时间倒序,那么此时查询结果的前三条信息就是我们需要加载的三条信息。

下载需要再加载剩下的3条怎么办?在我们每次查询加载的时候,都在内部记录了last_time = query.value("timestamp").toDateTime();一条上次加载的分界时间,最后通过SignalRouter存放在UserManager的哈希表中。

比如上面,加载完三条之后,这时候的timestamp记录为'2025-11-23 12:22:33'.

int count = 0;
QDateTime last_time;
while(query.next()){messages.push_back(createMessageFromQuery(query));count++;last_time = query.value("timestamp").toDateTime();
}
if (count > 0){emit SignalRouter::GetInstance().on_change_last_time(peerUid,last_time);
}// UserManager.h
std::unordered_map<int,QDateTime>_timestamp;

下次查询只需要在_timestamp查询上次记录的timestamp,就可以从上次的地方继续加载。上次的记录是'2025-11-23 12:22:33',接下来查询就是timestamp < '2025-11-23 12:22:33' ORDER BY timestamp DESC,查询到的就是最新的待加载的3条信息了。

这种分段加载的方式,在好友列表也是如此运用的,比如:


bool FriendsListPart::eventFilter(QObject *obj, QEvent *event)
{if (obj == friendsList->viewport() && event->type() == QEvent::Wheel) {QWheelEvent *wheelEvent = static_cast<QWheelEvent*>(event);QScrollBar *vScrollBar = friendsList->verticalScrollBar();if (vScrollBar) {// 自定义滚动步长int delta = wheelEvent->angleDelta().y();int step = delta > 0 ? -30 : 30;  // 反向,因为滚动条值增加是向下vScrollBar->setValue(vScrollBar->value() + step);int maxValue = vScrollBar->maximum();int currentValue = vScrollBar->value();if (currentValue - maxValue >= 0 && !UserManager::GetInstance()->IsLoadFriendsFinished()){qDebug() << "load more users";emit on_loading_users();}return true; // 事件已处理}}return QWidget::eventFilter(obj, event);
}

我们通过判断滚轮滑动,(当然这里还有判断,不必多言)得知要加载更多的好友,发出信号.

void FriendsListPart::do_loading_users()
{if(isLoading){return;}isLoading = true;// 动态获取信息for(auto&info:UserManager::GetInstance()->GetFriendsPerPage()){friendsModel->addFriend(FriendItem(info->id, info->status,info->sex,info->name,info->avatar,info->desc ));}QTimer::singleShot(1000,this,[this](){this->setLoading(false);});
}//UserManager.cppstd::vector<std::shared_ptr<UserInfo>>_friends;std::span<std::shared_ptr<UserInfo> > UserManager::GetFriendsPerPage(int size)
{if (size <= 0 || _friends_loaded >= _friends.size()) {return {};}int begin = _friends_loaded;int available =  _friends.size() - begin;int count  = std::min(size,available);_friends_loaded+=count;return std::span<std::shared_ptr<UserInfo>>(_friends).subspan(begin,count);
}

槽函数通过从UserManager的列表中获取信息,加入到model之中。每次加载固定的量size.

image-20251126155104800

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询