Chap20-Communication
Chap20-Communication
这一节拖得有点久,主要问题是涉及到的字段较多,写的时候太随意奔放导致遗留了很对字段没有设置,出现了很多莫名其妙的bug.因此,在编码的时候一定要捋清信号和槽的关系,每次点击都要处理好你我状态的设置。
本来只是想要简单的客户端请求,服务器给回复,但是考虑到对服务器的压力,因此,我们引入了本地的服务器Sqlite,用于在没有服务器连接的时候,也能存放一定的信息,同时减轻服务器的压力。也就是,能本地存,就本地存,默认不往服务器存。只有对方不在线的时候,才会存储在服务器,然后在对方上线之后,发给对方。至于本地和服务器的数据同步,也许以后会做。
数据库
服务器数据库

user

notifications

messages

friends

friend_apply

conversations

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

conversations

friends

可以看到本地数据库和服务器的数据库字段和结构有些不一样,比如对于服务器来说,不关心用户之间的关系,对他而言只是一个标志,对于本地数据库来说,用户是从自己的角度出发的所以考虑自己的关系,自己的好友。因此,本地数据库不存放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 ms
int32 env = 5;
MessageContent content =6;
}
struct MessageContent{
MessageType type; // 自定义类型
QVariant data; // 如果是文本文件,存放在这里,如果是二进制,此为空。
QString mimeType; // 具体的类型比如text/plain
QString fid; // 文件服务器需要用
};
struct MessageItem{
QString id; // 唯一的消息id
int to_id; // 接受者id
int from_id; // 发送者的id
QDateTime 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_ptr
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 (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 messages
WHERE ((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 conversations
WHERE 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_ptr
ids << 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 conversations
WHERE 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 conversations
WHERE 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 conversations
WHERE 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 messages
WHERE (from_uid = ? AND to_uid = ?) OR (from_uid = ? AND to_uid = ?)
ORDER BY timestamp desc
LIMIT 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 friends
WHERE 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 friends
WHERE 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 friends
WHERE 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 friends
WHERE 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_message
void 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, // 0
Group // 1
};
struct MessageContent{
MessageType type; // 自定义类型
QVariant data; // 如果是文本文件,存放在这里,如果是二进制,此为空。
QString mimeType; // 具体的类型比如text/plain
QString fid; // 文件服务器需要用
};
struct MessageItem{
QString id; // 唯一的消息id
int to_id; // 接受者id
int from_id; // 发送者的id
QDateTime 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; // 唯一id
int from_uid; // 自己
int to_uid; // 对方id
QDateTime 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信号中转器。如图所示:

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()){
// 添加message
emit 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,自己是from
conv.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 messages
WHERE ((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;
}

假如我们有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.cpp
std::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.


浙公网安备 33010602011771号