Chap20-Communication

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 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信号中转器。如图所示:

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()){
            // 添加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;
}

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

image-20251126155104800

posted @ 2025-12-24 23:18  大胖熊哈  阅读(1)  评论(0)    收藏  举报