微服务即时通讯系统的实现(客户端)----(3)

目录

  • [1. 聊天界面逻辑](#1. 聊天界面逻辑)
    • [1.1 发送消息](#1.1 发送消息)
    • [1.2 接收消息](#1.2 接收消息)
  • [2. 个人信息详情逻辑](#2. 个人信息详情逻辑)
    • [2.1 加载个人信息](#2.1 加载个人信息)
    • [2.2 修改昵称](#2.2 修改昵称)
    • [2.3 修改签名](#2.3 修改签名)
    • [2.4 修改电话 (1) - 发起短信验证码](#2.4 修改电话 (1) - 发起短信验证码)
    • [2.5 修改电话 (2) - 修改电话逻辑](#2.5 修改电话 (2) - 修改电话逻辑)
    • [2.6 修改头像](#2.6 修改头像)
  • [3. 用户详细信息界面逻辑](#3. 用户详细信息界面逻辑)
    • [3.1 获取指定用户的信息](#3.1 获取指定用户的信息)
    • [3.2 点击 "发送消息" 打开对应会话](#3.2 点击 "发送消息" 打开对应会话)
    • [3.3 删除好友](#3.3 删除好友)
    • [3.4 删除好友推送处理](#3.4 删除好友推送处理)
    • [3.5 发送好友申请](#3.5 发送好友申请)
  • [4. 主界面逻辑 (2)](#4. 主界面逻辑 (2))
    • [4.1 收到好友申请](#4.1 收到好友申请)
    • [4.2 同意好友申请](#4.2 同意好友申请)
    • [4.3 拒绝好友申请](#4.3 拒绝好友申请)
    • [4.4 获取到好友申请处理结果](#4.4 获取到好友申请处理结果)
  • [5. 小结](#5. 小结)

1. 聊天界面逻辑

1.1 发送消息

(1)客户端发送消息请求:

  • 在 MessageEditArea 中创建 initSignalSlot 方法。关联上 sendTextBtn的槽函数:
cpp 复制代码
void MessageEditArea::initSignalSlot()
{
 	DataCenter* dataCenter = DataCenter::getInstance();
 	// 处理按钮点击
 	connect(sendTextBtn, &QPushButton::clicked, this, &MessageEditArea::sendTextMessage);
}
  • 实现 MessageEditArea::sendTextMessage函数:
cpp 复制代码
void MessageEditArea::sendTextMessage()
{
    // 1. 先确认当前是否有会话选中了. 如果没有会话被选中, 则啥都不做.
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    if(dataCenter->getCurrentChatSessionId().isEmpty())
    {
        LOG() << "当前未选中任何会话, 不会发送消息!";
        // 上述日志, 只是在开发阶段能看到. 程序发布出去了, 此时就无法看到了.
        // 因此需要让普通用户, 也能看到 "提示"
        Toast::showMessage("当前未选中会话, 不发送任何消息!");
        return;
    }

    // 2. 获取到输入框的内容, 看输入框里是否有内容. 啥都没输入, 此时也不做任何操作.
    const QString& content = textEdit->toPlainText().trimmed();
    if(content.isEmpty())
    {
        LOG() << "输入框为空";
        return;
    }

    // 3. 清空输入框已有内容
    textEdit->setPlainText("");

    // 4. 通过网络发送数据给服务器
    dataCenter->sendTextMessageAsync(dataCenter->getCurrentChatSessionId(), content);
}
  • 实现 DataCenter::sendTextMessageAsync函数:
cpp 复制代码
void DataCenter::sendTextMessageAsync(const QString& chatSessionId, const QString& content)
{
    netClient.sendMessage(loginSessionId, chatSessionId, MessageType::TEXT_TYPE, content.toUtf8(), "");
}
  • 实现 NetClient::sendMessage函数以及接口定义:
cpp 复制代码
message NewMessageReq {
 	string request_id = 1;
 	optional string user_id = 2;
 	optional string session_id = 3;
 	string chat_session_id = 4;
 	MessageContent message = 5;
}
message NewMessageRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
}

// ⽅法实现,此⽅法同时⽀持四种消息的发送
// 此处的 extraInfo, 可以用来传递 "扩展信息" . 尤其是对于文件消息来说, 通过这个字段表示 "文件名"
// 其他类型的消息暂时不涉及, 就直接设为 "". 如果后续有消息类型需要, 都可以给这个参数, 赋予一定的特殊含义.
void NetClient::sendMessage(const QString &loginSessionId, const QString &chatSessionId, model::MessageType messageType,
                            const QByteArray &content, const QString& extraInfo)
{
    // 1. 通过 protobuf 构造 body
    bite_im::NewMessageReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setSessionId(loginSessionId);
    pbReq.setChatSessionId(chatSessionId);

    // 构造 MessageContent
    bite_im::MessageContent messageContent;
    if(messageType == model::TEXT_TYPE)
    {
        messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::STRING);

        bite_im::StringMessageInfo stringMessageInfo;
        stringMessageInfo.setContent(content);
        messageContent.setStringMessage(stringMessageInfo);
    }
    else if(messageType == model::IMAGE_TYPE)
    {
        messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::IMAGE);

        bite_im::ImageMessageInfo imageMessageInfo;
        imageMessageInfo.setFileId("");			// fileId 是文件在服务器存储的时候, 生成的 id, 此时还无法获取到, 暂时填成 ""
        imageMessageInfo.setImageContent(content);
        messageContent.setImageMessage(imageMessageInfo);
    }
    else if(messageType == model::FILE_TYPE)
    {
        messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::FILE);

        bite_im::FileMessageInfo fileMessageInfo;
        fileMessageInfo.setFileId(""); 			// fileId 是文件在服务器存储的时候, 生成的 id, 此时还无法获取到, 暂时填成 ""
        fileMessageInfo.setFileSize(content.size());
        fileMessageInfo.setFileName(extraInfo);
        fileMessageInfo.setFileContents(content);
        messageContent.setFileMessage(fileMessageInfo);
    }
    else if(messageType == model::SPEECH_TYPE)
    {
        messageContent.setMessageType(bite_im::MessageTypeGadget::MessageType::SPEECH);

        bite_im::SpeechMessageInfo speechMessageInfo;
        speechMessageInfo.setFileId(""); 			// fileId 是文件在服务器存储的时候, 生成的 id, 此时还无法获取到, 暂时填成 ""
        speechMessageInfo.setFileContents(content);
        messageContent.setSpeechMessage(speechMessageInfo);
    }
    else
    {
        LOG() << "错误的消息类型! messageType=" << messageType;
    }

    pbReq.setMessage(messageContent);

    // 序列化
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[发送消息] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", chatSessionId=" << pbReq.chatSessionId() << ", messageType=" << pbReq.message().messageType();

    QNetworkReply* resp = this->sendHttpRequest("/service/message_transmit/new_message", body);

    // 3. 处理 HTTP 响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 针对响应结果进行解析
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::NewMessageRsp>(resp, &ok, &reason);

        // b) 判定响应是否正确
        if(!ok)
        {
            LOG() << "[发送消息] 处理出错! reason=" << reason;
            return;
        }

        // c) 此处只是需要记录 "成功失败" , 不需要把内容写入到 DataCenter 中.

        // d) 通知调用者, 响应处理完毕
        emit dataCenter->sendMessageDone(messageType, content, extraInfo);

        // e) 打印日志
        LOG() << "[发送消息] 响应处理完毕! requestId=" << pbResp->requestId();
    });
}

(2)客户端收到消息响应:

  • 定义 DataCenter 信号:
cpp 复制代码
// 发送消息完成
void sendMessageDone(MessageType messageType, const QByteArray& content, constQString& extraInfo);
void sendMessageFailed(const QString& reason);
  • 修改 MessageEditArea::initSignalSlot,新增信号槽连接:
cpp 复制代码
// 处理发送消息的⽹络相应, 把⾃⼰发的内容添加到消息展⽰区
connect(dataCenter, &DataCenter::sendMessageDone, this, &MessageEditArea::addSelfMessage);
connect(dataCenter, &DataCenter::sendMessageFailed, this, [=](const QString& reason) 
{
 	Toast::showMessage("发送消息失败! " + reason);
});
  • 新增函数 MessageEditArea::addSelfMessage函数将消息添加到消息显示区:
cpp 复制代码
void MessageEditArea::addSelfMessage(model::MessageType messageType, const QByteArray& content, const QString& extraInfo)
{
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    const QString& currentChatSessionId = dataCenter->getCurrentChatSessionId();

    // 1. 构造出一个消息对象
    Message message = Message::makeMessage(messageType, currentChatSessionId, *dataCenter->getMyself(), content, extraInfo);
    dataCenter->addMessage(message);

    // 2. 把这个新的消息, 显示到消息展示区
    MainWidget* mainWidget = MainWidget::getInstance();
    MessageShowArea* messageShowArea = mainWidget->getMessageShowArea();
    messageShowArea->addMessage(false, message);

    // 3. 控制消息显示区, 滚动条, 滚动到末尾.
    messageShowArea->scrollToEnd();

    // 4. 发送信号, 通知会话列表, 更新最后一条消息
    emit dataCenter->updateLastMessage(currentChatSessionId);
}
  • 给 DataCenter 定义信号。更新会话列表中的 "最后⼀条消息":
cpp 复制代码
// 更新会话列表中的最后⼀条消息
void updateLastMessage(const QString& chatSessionId);
  • 在 SessionArea 的 SessionItem 构造函数中,连接上述信号并处理:
cpp 复制代码
SessionItem::SessionItem(QWidget* owner, const QString& chatSessionId, const QIcon& avatar,
            const QString& name, const QString& lastMessage)
    :SessionFriendItem(owner, avatar, name, lastMessage)
    ,chatSessionId(chatSessionId)
    ,text(lastMessage)
{
    // 处理更新最后一条信息的信号
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    connect(dataCenter, &model::DataCenter::updateLastMessage, this, &SessionItem::updateLastMessage);

    // 需要显示出未读消息的数目, 为了支持客户端重启之后, 未读消息仍然能正确显示.
    int unread = dataCenter->getUnread(chatSessionId);
    if(unread > 0)
    {
        // 存在未读消息
        this->messageLabel->setText(QString("[未读%1条] ").arg(unread) + text);
    }
}

void SessionItem::updateLastMessage(const QString& chatSessionId)
{
    model::DataCenter* dataCenter = model::DataCenter::getInstance();

    // 1. 判定 chatSessionId 是否匹配
    if(this->chatSessionId != chatSessionId)
    {
        // 当前 SessionItem 不是你正在发消息的 SessionItem!
        return;
    }

    // chatSessionId 匹配, 真正更新最后一条消息!!
    // 2. 把最后一条消息, 获取到.
    QList<Message>* messageList = dataCenter->getRecentMessageList(chatSessionId);
    if(messageList == nullptr || messageList->size() == 0)
    {
        // 当前会话没有任何消息, 无需更新
        return;
    }

    const Message& lastMessage = messageList->back();

    // 3. 明确显示的文本内容
    //    由于消息有四种类型.
    //    文本消息, 直接显示消息的内容; 图片消息, 直接显示 "[图片]"; 文件消息, 直接显示 "[文件]"; 语音消息, 直接显示 "[语音]"
    if(lastMessage.messageType == model::TEXT_TYPE)
    {
        text = lastMessage.content;
    }
    else if(lastMessage.messageType == model::IMAGE_TYPE)
    {
        text = "[图片]";
    }
    else if(lastMessage.messageType == model::FILE_TYPE)
    {
        text = "[文件]";
    }
    else if(lastMessage.messageType == model::SPEECH_TYPE)
    {
        text = "[语音]";
    }
    else
    {
        LOG() << "错误的消息类型!";
        return;
    }

    // 4. 把这个内容, 显示到界面上
    //    针对这里的逻辑, 后续还需要考虑到 "未读消息" 情况. 关于未读消息的处理, 后续编写 "接收消息" 的时候再处理.
    //    先判定, 当前消息的会话, 是不是正在选中的会话. 如果是, 不会更新任何未读消息.
    //    如果不是, 看未读消息是否 > 0, 并且做出前缀的拼装
    if(chatSessionId == dataCenter->getCurrentChatSessionId())
    {
        this->messageLabel->setText(text);
    }
    else
    {
        int unread = dataCenter->getUnread(chatSessionId);
        if(unread > 0)
        {
            this->messageLabel->setText(QString("[未读%1条] ").arg(unread) + text);
        }
    }
}
  • 实现对于未读消息数据的处理:
cpp 复制代码
void DataCenter::clearUnread(const QString& chatSessionId)
{
    (*unreadMessageCount)[chatSessionId] = 0;

    // 手动保存一下结果到文件中.
    saveDataFile();
}

void DataCenter::addUnread(const QString& chatSessionId)
{
    ++(*unreadMessageCount)[chatSessionId];

    // 手动保存一下结果到文件中.
    saveDataFile();
}

int DataCenter::getUnread(const QString& chatSessionId)
{
    return (*unreadMessageCount)[chatSessionId];
}
  • 补充 SessionItem 中的未读消息处理:
cpp 复制代码
void SessionItem::active()
{
    // 点击之后, 要加载会话的历史消息列表
    LOG() << "点击 SessionItem 触发的逻辑! chatSessionId=" << chatSessionId;

    // 加载会话历史消息, 即会涉及到当前内存的数据操作, 又会涉及到网络通信, 还涉及到界面的变更.
    MainWidget* mainWidget = MainWidget::getInstance();
    mainWidget->loadRecentMessage(chatSessionId);

    // 清空未读消息的数据, 并且更新显示
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    dataCenter->clearUnread(chatSessionId);

    // 更新界面的显示. 把会话消息预览这里, 前面的 "[未读x条]" 内容给干掉
    this->messageLabel->setText(text);
}

(3)服务器实现逻辑:

  • 注册路由:
cpp 复制代码
httpServer.route("/service/message_transmit/new_message", [=](const QHttpServerRequest& req) 
{
 	return this->sendMessage(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::newMessage(const QHttpServerRequest &req)
{
    // 解析请求
    bite_im::NewMessageReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 发送消息] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", chatSessionId=" << pbReq.chatSessionId() << ", messageType=" << pbReq.message().messageType();

    if (pbReq.message().messageType() == bite_im::MessageTypeGadget::MessageType::STRING)
    {
        LOG() << "发送的消息内容=" << pbReq.message().stringMessage().content();
    }

    // 构造响应
    bite_im::NewMessageRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");

    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

1.2 接收消息

(1)客户端实现逻辑:

  • 在 NetClient 中实现 handleWsMessage 处理 websocket 收到的数据:
cpp 复制代码
void NetClient::handleWsMessage(const model::Message& message)
{
    // 这里要考虑两个情况
    QList<model::Message>* messageList = dataCenter->getRecentMessageList(message.chatSessionId);
    if(messageList == nullptr)
    {
        // 1. 如果当前这个消息所属的会话, 里面的消息列表, 没有在本地加载, 此时就需要通过网络先加载整个消息列表.
        connect(dataCenter, &model::DataCenter::getRecentMessageListDoneNoUI, this, &NetClient::receiveMessage, Qt::UniqueConnection);
        dataCenter->getRecentMessageListAsync(message.chatSessionId, false);
    }
    else
    {
        // 2. 如果当前这个消息所属的会话, 里面的消息已经在本地加载了, 直接把这个消息尾插到消息列表中即可.
        messageList->push_back(message);
        this->receiveMessage(message.chatSessionId);
    }
}
  • 实现 DataCenter::receiveMessage函数:
cpp 复制代码
void NetClient::receiveMessage(const QString& chatSessionId)
{
    // 先需要判定一下, 当前这个收到的消息对应的会话, 是否是正在被用户选中的 "当前会话"
    // 当前会话, 就需要把消息, 显示到消息展示区, 也需要更新会话列表的消息预览
    // 不是当前会话, 只需要更新会话列表中的消息预览, 并且更新 "未读消息数目"
    if(chatSessionId == dataCenter->getCurrentChatSessionId())
    {
        // 收到的消息会话, 就是选中会话
        // 在消息展示区, 新增一个消息
        const model::Message& lastMessage = dataCenter->getRecentMessageList(chatSessionId)->back();
        // 通过信号, 让 NetClient 模块, 能够通知界面(消息展示区)
        emit dataCenter->receiveMessageDone(lastMessage);
    }
    else
    {
        // 收到的消息会话, 不是选中会话
        // 更新未读消息数目
        dataCenter->addUnread(chatSessionId);
    }

    // 统一更新会话列表的消息预览
    emit dataCenter->updateLastMessage(chatSessionId);
}
  • 定义 DataCenter 信号:
cpp 复制代码
// 收到消息
void receiveMessageDone(const Message& message);
  • 修改 MessageEditArea::initSignalSlot,添加信号槽:
cpp 复制代码
// 处理收到网络上来自别人的响应情况
connect(dataCenter, &DataCenter::receiveMessageDone, this, &MessageEditArea::addOtherMessage);
  • 实现 MessageEditArea::addOtherMessage函数:
cpp 复制代码
void MessageEditArea::addOtherMessage(const model::Message &message)
{
    // 1. 通过主界面, 拿到消息展示区.
    MainWidget* mainWidget = MainWidget::getInstance();
    MessageShowArea* messageShowArea = mainWidget->getMessageShowArea();

    // 2. 把收到的新的消息, 添加到消息展示区
    messageShowArea->addMessage(true, message);

    // 3. 控制消息展示区的滚动条, 把窗口滚动到末尾
    messageShowArea->scrollToEnd();

    // 4. 提示一个收到消息
    Toast::showMessage("收到新消息!");
}

(2)服务器实现逻辑:

  • 在界面上创建⼀个按钮,表示 "发送文本消息",并实现信号槽:
cpp 复制代码
void Widget::on_pushButton_clicked()
{
 	WebsocketServer* websocketServer = WebsocketServer::getInstance();
 	emit websocketServer->sendTextResp();
}
  • 给 WebsocketServer 创建信号 sendTextResp
cpp 复制代码
signals:
	void sendTextResp();
  • 实现处理函数:注意此处的 connect 要放到 connect(&websocketServer,
    &QWebSocketServer::newConnection, this, [=] () { } ) 当中这样才能捕获到 socket 对象:
cpp 复制代码
connect(this, &WebsocketServer::sendTextResp, this, [=]()
{
	// 此处就可以捕获到 socket 对象, 从而可以通过 socket 对象给客户端返回数据.
    if(socket == nullptr || !socket->isValid())
    {
		LOG() << "socket 对象无效!";
        return;
    }

    // 构造响应数据
    QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");
    bite_im::MessageInfo messageInfo = makeTextMessageInfo(this->messageIndex++, "2000", avatar);

    bite_im::NotifyNewMessage notifyNewMessage;
    notifyNewMessage.setMessageInfo(messageInfo);

    bite_im::NotifyMessage notifyMessage;
    notifyMessage.setNotifyEventId("");
    notifyMessage.setNotifyType(bite_im::NotifyTypeGadget::NotifyType::CHAT_MESSAGE_NOTIFY);
    notifyMessage.setNewMessageInfo(notifyNewMessage);

    // 序列化
    QByteArray body = notifyMessage.serialize(&this->serializer);

    // 发送消息给客户端
    socket->sendBinaryMessage(body);

    LOG() << "发送文本消息响应";
});
  • 在 QWebSocket::disconnected 处理函数中,添加解除信号槽的逻辑:
cpp 复制代码
// 针对这个 socket 对象, 进行剩余信号的处理
connect(socket, &QWebSocket::disconnected, this, [=]()
{
	qDebug() << "[websocket] 连接断开!";
	disconnect(this, &WebsocketServer::sendTextResp, this, nullptr);
}

此处的 disconnect 非常重要。否则如果客户端重复连接服务器,服务器就会尝试针对上次已经释放的socket 对象进行处理,就会使程序崩溃。

2. 个人信息详情逻辑

2.1 加载个人信息

(1)直接从 DataCenter 中读取数据:在 SelfInfoWidget 构造函数中, 添加数据加载:

cpp 复制代码
// 11. 加载数据到界面上
model::DataCenter* dataCenter = model::DataCenter::getInstance();
model::UserInfo* myself = dataCenter->getMyself();
if (myself != nullptr)
{
	// 就把个人信息, 显示到界面上
	avatarBtn->setIcon(myself->avatar);
	idLabel->setText(myself->userId);
	nameLabel->setText(myself->nickname);
	descLabel->setText(myself->description);
	phoneLabel->setText(myself->phone);
}

2.2 修改昵称

(1)客户端发送请求:

  • 在 SelfInfoWidget 构造函数连接信号槽并实现切换显示状态:
cpp 复制代码
void SelfInfoWidget::initSingalSlot()
{
    connect(nameModifyBtn, &QPushButton::clicked, this, [=]()
    {
        // 把当前的 nameLabel 和 nameModifyBtn 隐藏起来
        nameLabel->hide();
        nameModifyBtn->hide();
        layout->removeWidget(nameLabel);
        layout->removeWidget(nameModifyBtn);

        // 把 nameEdit 和 nameSubmitBtn 显示出来
        nameEdit->show();
        nameSubmitBtn->show();
        layout->addWidget(nameEdit, 1, 2);
        layout->addWidget(nameSubmitBtn, 1, 3);
        // 把输入框的内容进行设置.
        nameEdit->setText(nameLabel->text());
    });

   	connect(nameSubmitBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickNameSubmitBtn);
}
  • 实现 SelfInfoWidget::clickNameSubmitBtn函数:
cpp 复制代码
void SelfInfoWidget::clickNameSubmitBtn()
{
    // 1. 从输入框中, 拿到修改后的昵称
    const QString& nickname = nameEdit->text();
    if(nickname.isEmpty())
    {
        return;
    }

    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    connect(dataCenter, &model::DataCenter::changeNicknameDone, this, &SelfInfoWidget::clickNameSubmitBtnDone, Qt::UniqueConnection);
    dataCenter->changeNicknameAsync(nickname);
}
  • 实现 DataCenter::changeNickNameAsync函数:
cpp 复制代码
// 修改昵称
void DataCenter::changeNickNameAsync(const QString &nickName)
{
 	netClient.changeNickName(loginSessionId, nickName);
}
  • 实现 NetClient::changeNickName函数和接口定义:
cpp 复制代码
//----------------------------
//⽤⼾昵称修改
message SetUserNicknameReq {
 	string request_id = 1;
 	optional string user_id = 2;
 	optional string session_id = 3;
 	string nickname = 4;
}
message SetUserNicknameRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
}

// 函数实现:
void NetClient::changeNickname(const QString& loginSessionId, const QString& nickname)
{
    // 1. 通过 protobuf 构造请求 body
    bite_im::SetUserNicknameReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setSessionId(loginSessionId);
    pbReq.setNickname(nickname);
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[修改用户昵称] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", nickname=" << pbReq.nickname();

    // 2. 发送 http 请求
    QNetworkReply* resp = sendHttpRequest("/service/user/set_nickname", body);

    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::SetUserNicknameRsp>(resp, &ok, &reason);

        // b) 判定是否出错
        if(!ok)
        {
            LOG() << "[修改用户昵称] 出错! reason=" << reason;
            return;
        }

        // c) 把数据设置到 DataCenter 里面. 这里的处理和前面不太一样.
        dataCenter->resetNickname(nickname);

        // d) 发送信号, 通知调用者, 这里处理完毕
        emit dataCenter->changeNicknameDone();

        // e) 打印日志
        LOG() << "[修改用户昵称] 处理响应完毕! requestId=" << pbResp->requestId();
    });
}

(2)客户端处理响应:

  • 实现 DataCenter::resetNickName
cpp 复制代码
void DataCenter::resetNickName(const QString& nickName)
{
 	myself->nickname = nickName;
}
  • 定义 DataCenter 信号:
cpp 复制代码
void changeNickNameDone();
  • 实现 SelfInfoWidget::clickNameSubmitBtnDone函数:
cpp 复制代码
void SelfInfoWidget::clickNameSubmitBtnDone()
{
    // 对界面控件进行切换. 把刚才输入框切换回 label, 把提交按钮切换回编辑按钮.
    // 同时还需要把输入框中的本文设置为 label 中的文本.
    layout->removeWidget(nameEdit);
    nameEdit->hide();
    layout->addWidget(nameLabel, 1, 2);
    nameLabel->show();
    nameLabel->setText(nameEdit->text());

    layout->removeWidget(nameSubmitBtn);
    nameSubmitBtn->hide();
    layout->addWidget(nameModifyBtn, 1, 3);
    nameModifyBtn->show();
}
  • 修改 MessageShowArea 的 MessageItem::makeMessageItem,自动更新消息展示区的消息中显示的昵称:
cpp 复制代码
// 6. 当用户修改了昵称的时候, 同步修改此处的用户昵称.
if(!isLeft)
{
	model::DataCenter* dataCenter = model::DataCenter::getInstance();
	connect(dataCenter, &model::DataCenter::changeNicknameDone, messageItem, [=]()
	{
		nameLabel->setText(dataCenter->getMyself()->nickname + " | " + message.time);
	});

	connect(dataCenter, &model::DataCenter::changeAvatarDone, messageItem, [=]()
	{
		UserInfo* myself = dataCenter->getMyself();
		avatarBtn->setIcon(myself->avatar);
	});
}

(3)服务器实现逻辑:

  • 注册路由:
cpp 复制代码
httpServer.route("/service/user/set_nickname", [=](const QHttpServerRequest& req) 
{
 	return this->setNickName(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::setNickname(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::SetUserNicknameReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 修改用户昵称] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", nickname=" << pbReq.nickname();

    // 构造响应
    bite_im::SetUserNicknameRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");
    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

2.3 修改签名

(1)客户端发送请求:

  • 在 SelfInfoWidget 构造函数连接信号槽:
cpp 复制代码
void SelfInfoWidget::initSingalSlot()
{
    connect(descModifyBtn, &QPushButton::clicked, this, [=]()
    {
        descLabel->hide();
        descModifyBtn->hide();
        layout->removeWidget(descLabel);
        layout->removeWidget(descModifyBtn);

        descEdit->show();
        descSubmitBtn->show();
        layout->addWidget(descEdit, 2, 2);
        layout->addWidget(descSubmitBtn, 2, 3);

        descEdit->setText(descLabel->text());
    });
    
	connect(descSubmitBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickDescSubmitBtn);
}
  • 实现 SelfInfoWidget::clickSignatureSubmitBtn函数:
cpp 复制代码
void SelfInfoWidget::clickDescSubmitBtn()
{
    // 1. 从输入框中, 拿到修改后的签名内容
    const QString& desc = descEdit->text();
    if(desc.isEmpty())
    {
        return;
    }

    // 2. 发送网络请求
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    connect(dataCenter, &model::DataCenter::changeDescriptionDone, this, &SelfInfoWidget::chickDescSubmitBtnDone, Qt::UniqueConnection);
    dataCenter->changeDescriptionAsync(desc);
}
  • 实现 DataCenter::changeDescriptionAsync函数:
cpp 复制代码
void DataCenter::changeDescriptionAsync(const QString &description)
{
 	netClient.changeDescription(loginSessionId, description);
}
  • 实现 NetClient::changeDescription和接口定义:
cpp 复制代码
//----------------------------
//⽤⼾签名修改
message SetUserDescriptionReq {
 	string request_id = 1;
 	optional string user_id = 2;
 	optional string session_id = 3;
 	string description = 4;
}

message SetUserDescriptionRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
}

// 函数实现
void NetClient::changeDescription(const QString& loginSessionId, const QString& desc)
{
    // 1. 通过 protobuf 构造请求 body
    bite_im::SetUserDescriptionReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setSessionId(loginSessionId);
    pbReq.setDescription(desc);
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[修改签名] 发送请求 requestId=" << pbReq.requestId() << ", loginSessisonId=" << pbReq.sessionId()
          << ", desc=" << pbReq.description();

    QNetworkReply* resp = this->sendHttpRequest("/service/user/set_description", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::SetUserDescriptionRsp>(resp, &ok, &reason);

        // b) 判定响应是否成功
        if(!ok)
        {
            LOG() << "[修改签名] 响应失败! reason=" << reason;
            return;
        }

        // c) 把得到的结果, 写入 DataCenter
        dataCenter->resetDescription(desc);

        // d) 发送信号, 通知修改完成
        emit dataCenter->changeDescriptionDone();

        // e) 打印日志
        LOG() << "[修改签名] 响应完成! requestId=" << pbResp->requestId();
    });
}

(2)客户端处理响应:

  • 实现 DataCenter::resetDescription函数:
cpp 复制代码
void DataCenter::resetDescription(const QString &description)
{
 	myself->description = description;
}
  • 定义 DataCenter 信号:
cpp 复制代码
void changeDescriptionDone();
  • 实现 SelfInfoWidget::chickDescSubmitBtnDone函数:
cpp 复制代码
void SelfInfoWidget::chickDescSubmitBtnDone()
{
    // 切换界面.
    // 把 label 替换回输入框, 把编辑按钮替换回修改按钮
    layout->removeWidget(descEdit);
    descEdit->hide();
    layout->addWidget(descLabel, 2, 2);
    descLabel->show();
    descLabel->setText(descEdit->text());

    layout->removeWidget(descSubmitBtn);
    descSubmitBtn->hide();
    layout->addWidget(descModifyBtn, 2, 3);
    descModifyBtn->show();
}

(3)服务器实现逻辑:

  • 注册路由:
cpp 复制代码
httpServer.route("/service/user/set_description", [=](const QHttpServerRequest& req) 
{
 	return this->setDesc(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::setDesc(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::SetUserDescriptionReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 修改用户签名] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", desc=" << pbReq.description();

    // 构造响应
    bite_im::SetUserDescriptionRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");
    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

2.4 修改电话 (1) - 发起短信验证码

(1)客户端发送请求:

  • 在 SelfInfoWidget 构造函数连接信号槽:
cpp 复制代码
void SelfInfoWidget::initSingalSlot()
{
    connect(phoneModifyBtn, &QPushButton::clicked, this, [=]()
    {
        phoneLabel->hide();
        phoneModifyBtn->hide();
        layout->removeWidget(phoneLabel);
        layout->removeWidget(phoneModifyBtn);

        phoneEdit->show();
        phoneSubmitBtn->show();
        layout->addWidget(phoneEdit, 3, 2);
        layout->addWidget(phoneSubmitBtn, 3, 3);

        verifyCodeTag->show();
        verifyCodeEdit->show();
        getVerifyCodeBtn->show();
        layout->addWidget(verifyCodeTag, 4, 1);
        layout->addWidget(verifyCodeEdit, 4, 2);
        layout->addWidget(getVerifyCodeBtn, 4, 3);

        phoneEdit->setText(phoneLabel->text());
    });

    connect(getVerifyCodeBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickGetVerifyCodeBtn);
    connect(phoneSubmitBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickPhoneSubmitBtn);
}
  • 实现 clickGetVerifyCodeBtn发送验证码:
  • 注意:
    • 需要在 SelfInfoWidget 中把发送验证码的手机号存起来,并在后续发送修改请求的时候使用这⼀个号码来请求。
    • 确保重新绑定的手机号码和发送验证码的手机号码⼀致。(发送修改手机号请求的时候手机号码不能从输入框读取!)。
cpp 复制代码
void SelfInfoWidget::clickGetVerifyCodeBtn()
{
    // 1. 获取到输入框中的手机号码
    const QString& phone = phoneLabel->text();
    if(phone == nullptr)
    {
        return;
    }

    // 2. 给服务器发起请求.
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    connect(dataCenter, &model::DataCenter::getVerifyCodeDone, this, [=]()
    {
        // 不需要做其他的处理, 只需要提示一下, 验证码已经发送
        Toast::showMessage("短信验证码已经发送");
    });

    dataCenter->getVerifyCodeAsync(phone);

    // 3. 把刚才发送请求的手机号码, 保存起来.
    //    后续点击提交按钮, 修改电话, 修改的号码, 不从输入框读取, 而是读取这个变量.
    this->phoneToChange = phone;

    // 4. 禁用发送验证码按钮, 并给出倒计时
    this->getVerifyCodeBtn->setEnabled(false);

    leftTime = 30;
    QTimer* timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, [=]()
    {
        if(leftTime <= 1)
        {
            // 倒计时结束了
            getVerifyCodeBtn->setEnabled(true);
            getVerifyCodeBtn->setText("获取验证码");
            timer->stop();
            timer->deleteLater();
            return;
        }

        --leftTime;
        getVerifyCodeBtn->setText(QString::number(leftTime) + "s");
    });

    timer->start(1000);
}
  • 实现 DataCenter::getVerifyCodeAsync函数:
cpp 复制代码
void DataCenter::getVerifyCodeAsync(const QString& phone)
{
 	netClient.getVerifyCode(phone);
}
  • 实现 NetClient::getVerifyCode函数和接口定义:
cpp 复制代码
//----------------------------
//⼿机号验证码获取
message PhoneVerifyCodeReq {
 	string request_id = 1;
 	string phone_number = 2;
}
message PhoneVerifyCodeRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3;
 	string verify_code_id = 4;
}
// 函数实现
void NetClient::getVerifyCode(const QString& phone)
{
    // 1. 通过 protobuf 构造请求 body
    bite_im::PhoneVerifyCodeReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setPhoneNumber(phone);
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[获取手机验证码] 发送请求 requestId=" << pbReq.requestId() << ", phone=" << phone;

    // 2. 发送 HTTP 请求
    QNetworkReply* resp = this->sendHttpRequest("/service/user/get_phone_verify_code", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::PhoneVerifyCodeRsp>(resp, &ok, &reason);

        // b) 判定响应是否成功
        if(!ok)
        {
            LOG() << "[获取手机验证码] 失败! reason=" << reason;
            return;
        }

        // c) 保存数据到 DataCenter
        dataCenter->resetVerifyCodeId(pbResp->verifyCodeId());

        // d) 发送信号, 通知调用者
        emit dataCenter->getVerifyCodeDone();

        // e) 打印日志
        LOG() << "[获取手机验证码] 响应完成 requestId=" << pbResp->requestId();
    });
}

服务器会在 redis 中保存 verify_code_id 和 verify_code,给后续的验证提供支持。

(2)客户端处理响应:

  • 实现 DataCenter::resetVerifyCodeId函数:
cpp 复制代码
void DataCenter::resetVerifyCodeId(std::shared_ptr<bite_im::PhoneVerifyCodeRsp> resp)
{
 	this->currentVerifyCodeId = resp->verifyCodeId();
}
  • 定义 DataCenter 信号:
cpp 复制代码
void getVerifyCodeDone();

这个信号暂时不使用。会在后续的 "手机号登录" 功能中使用。

(3)服务器实现逻辑:

  • 注册路由:
cpp 复制代码
httpServer.route("/service/user/get_phone_verify_code", [=](const QHttpServerRequest& req) 
{
 	return this->getPhoneVerifyCode(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::getPhoneVerifyCode(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::PhoneVerifyCodeReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 获取短信验证码] requestId=" << pbReq.requestId() << ", phone=" << pbReq.phoneNumber();

    // 构造响应 body
    bite_im::PhoneVerifyCodeRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");
    pbResp.setVerifyCodeId("testVerifyCodeId");
    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

2.5 修改电话 (2) - 修改电话逻辑

(1)客户端发送请求:

  • 实现 SelfInfoWidget::clickPhoneSubmitBtn函数:
  • 注意:
    • 需要在 SelfInfoWidget 中把发送验证码的手机号存起来,并在后续发送修改请求的时候使用这⼀个号码来请求。
    • 确保重新绑定的手机号码和发送验证码的手机号码⼀致。(发送修改⼿机号请求的时候手机号码不能从输入框读取!)。
cpp 复制代码
void SelfInfoWidget::clickPhoneSubmitBtn()
{
    // 1. 先判定, 当前验证码是否已经收到.
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    QString verifyCodeId = dataCenter->getVerifyCodeId();
    if(verifyCodeId.isEmpty())
    {
        // 服务器这边还没有返回验证码响应呢
        // LOG() << "服务器尚未返回验证码! 稍后重试!";
        Toast::showMessage("服务器尚未返回响应, 稍后重试!");
        return;
    }

    // 如果当前已经拿到 verifyCodeId, 就可以清空 DataCenter 中存储的值. 确保下次点击提交按钮的时候, 上述逻辑仍然有效
    dataCenter->resetVerifyCodeId("");

    // 2. 获取到用户输入的验证码
    QString verifyCode = verifyCodeEdit->text();
    if(verifyCode.isEmpty())
    {
        Toast::showMessage("验证码不能为空!");
        return;
    }

    verifyCodeEdit->setText("");  // 获取到验证码之后, 就可以清空了.
    // 3. 发送请求, 把当前验证码信息, 发送给服务器
    connect(dataCenter, &model::DataCenter::changePhoneDone, this, &SelfInfoWidget::clickPhoneSubmitBtnDone, Qt::UniqueConnection);
    dataCenter->changePhoneAsync(this->phoneToChange, verifyCodeId, verifyCode);

    // 4. 让验证码按钮的倒计时停止. 把 leftTime 设为 1, 就可以停止了
    leftTime = 1;
}
  • 实现 DataCenter::changePhoneAsync函数
cpp 复制代码
void DataCenter::changePhoneAsync(const QString &phone, const QString& verifyCodeId, const QString& verifyCode)
{
 	netClient.changePhone(loginSessionId, phone, verifyCodeId, verifyCode);
}
  • 实现 NetClient::changePhone函数和接口定义:
cpp 复制代码
//----------------------------
//⽤⼾⼿机修改
message SetUserPhoneNumberReq {
 	string request_id = 1;
 	optional string user_id = 2;
 	optional string session_id = 3;
 	string phone_number = 4;
 	string phone_verify_code_id = 5;
 	string phone_verify_code = 6;
}
message SetUserPhoneNumberRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
}

// 函数实现
void NetClient::changePhone(const QString& loginSessionId, const QString& phone, const QString& verifyCodeId, const QString& verifyCode)
{
    // 1. 通过 protobuf 构造请求 body
    bite_im::SetUserPhoneNumberReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setSessionId(loginSessionId);
    pbReq.setPhoneNumber(phone);
    pbReq.setPhoneVerifyCodeId(verifyCodeId);
    pbReq.setPhoneVerifyCode(verifyCode);
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[修改手机号] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", phone=" << pbReq.phoneNumber() << ", verifyCodeId=" << pbReq.phoneVerifyCodeId() << ", verifyCode=" << pbReq.phoneVerifyCode();

    // 2. 发送 http 请求
    QNetworkReply* resp = sendHttpRequest("/service/user/set_phone", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::SetUserPhoneNumberRsp>(resp, &ok, &reason);

        // b) 判定响应是否正确
        if(!ok)
        {
            LOG() << "[修改手机号] 响应失败! reason=" << reason;
            return;
        }

        // c) 把结果记录到 DataCenter 中
        dataCenter->resetPhone(phone);

        // d) 发送信号, 通知调用者完成
        emit dataCenter->changePhoneDone();

        // e) 打印日志
        LOG() << "[修改手机号] 相应完成 requestId=" << pbResp->requestId();
    });
}

(2)客户端处理响应:

  • 实现 DataCenter::resetPhone函数:
cpp 复制代码
void DataCenter::resetPhone(const QString &phone)
{
 	myself->phone = phone;
}
  • 定义 DataCenter 信号:
cpp 复制代码
// 修改手机号完成
void changePhoneDone();
  • 实现 SelfInfoWidget::clickPhoneSubmitBtnDone函数:
cpp 复制代码
void SelfInfoWidget::clickPhoneSubmitBtnDone()
{
    layout->removeWidget(verifyCodeTag);
    layout->removeWidget(verifyCodeEdit);
    layout->removeWidget(getVerifyCodeBtn);
    layout->removeWidget(phoneEdit);
    layout->removeWidget(phoneSubmitBtn);
    verifyCodeTag->hide();
    verifyCodeEdit->hide();
    getVerifyCodeBtn->hide();
    phoneEdit->hide();
    phoneSubmitBtn->hide();

    layout->addWidget(phoneLabel, 3, 2);
    phoneLabel->show();
    phoneLabel->setText(this->phoneToChange);
    layout->addWidget(phoneModifyBtn, 3, 3);
    phoneModifyBtn->show();
}

(3)服务器实现逻辑:

  • 注册路由:
cpp 复制代码
httpServer.route("/service/user/set_phone", [=](const QHttpServerRequest& req) 
{
 	return this->setPhone(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::setPhone(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::SetUserPhoneNumberReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 修改手机号] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId() << ", phone=" << pbReq.phoneNumber()
          << ", verifyCodeId=" << pbReq.phoneVerifyCodeId() << ", verifyCode=" << pbReq.phoneVerifyCode();

    // 构造响应 body
    bite_im::SetUserPhoneNumberRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");
    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

2.6 修改头像

(1)客户端发送请求:

  • 在 SelfInfoWidget 构造函数连接信号槽:
cpp 复制代码
connect(avatarBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickAvatarBtn);
}
  • 实现 SelfInfoWidget::clickAvatar函数:
cpp 复制代码
void SelfInfoWidget::clickAvatarBtn()
{
    // 1. 弹出对话框, 选择文件
    QString filter = "Image Files (*.png *.jpg *.jpeg)";
    QString imagePath = QFileDialog::getOpenFileName(this, "选择头像", QDir::homePath(), filter);
    if(imagePath.isEmpty())
    {
        // 用户取消了
        LOG() << "用户未选择任何头像";
        return;
    }

    // 2. 根据路径, 读取到图片的内容.
    QByteArray imageBytes = model::loadFileToByteArray(imagePath);

    // 3. 发送请求, 修改头像
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    connect(dataCenter, &model::DataCenter::changeAvatarDone, this, &SelfInfoWidget::clickAvatarBtnDone, Qt::UniqueConnection);
    dataCenter->changeAvatarAsync(imageBytes);
}
  • 实现 DataCenter::changeAvatarAsync函数:
cpp 复制代码
void DataCenter::changeAvatarAsync(const QByteArray &avatar)
{
 	netClient.changeAvatar(loginSessionId, avatar);
}
  • 实现 NetClient::changeAvatar函数和接口定义:
cpp 复制代码
//----------------------------
//⽤⼾头像修改
message SetUserAvatarReq {
 	string request_id = 1;
 	optional string user_id = 2;
 	optional string session_id = 3;
 	bytes avatar = 4;
}
message SetUserAvatarRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
}

// 函数实现
void NetClient::changeAvatar(const QString& loginSessionId, const QByteArray& avatar)
{
    // 1. 通过 protobuf 构造请求 body
    bite_im::SetUserAvatarReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setSessionId(loginSessionId);
    pbReq.setAvatar(avatar);
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[修改头像] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId();

    // 2. 发送 http 请求
    QNetworkReply* resp = sendHttpRequest("/service/user/set_avatar", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::SetUserAvatarRsp>(resp, &ok, &reason);

        // b) 判定响应结果是否正确
        if(!ok)
        {
            LOG() << "[修改头像] 响应出错! reason=" << reason;
            return;
        }

        // c) 把数据设置到 DataCenter 中
        dataCenter->resetAvatar(avatar);

        // d) 发送信号
        emit dataCenter->changeAvatarDone();

        // e) 打印日志
        LOG() << "[修改头像] 响应完成 requestId=" << pbResp->requestId();
    });
}

(2)客户端处理响应:

  • 实现 DataCenter::resetAvatar函数:
cpp 复制代码
void DataCenter::resetAvatar(const QByteArray &avatar)
{
 	myself->avatar = makeIcon(avatar);
}
  • 定义 DataCenter 信号:
cpp 复制代码
void changeAvatarDone();
  • 实现 SelfInfoWidget::clickAvatarDone函数来修改用户详情界面的头像:
cpp 复制代码
void SelfInfoWidget::clickAvatarBtnDone()
{
    // 把设置的头像, 更新到界面上.
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    avatarBtn->setIcon(dataCenter->getMyself()->avatar);
}
  • 修改主界面的头像。在 MainWidget::initData 中处理 changeAvatarDone 信号:
cpp 复制代码
connect(dataCenter, &DataCenter::changeAvatarDone, this, [=]()
{
	UserInfo* myself = dataCenter->getMyself();
	userAvatar->setIcon(myself->avatar);
});
  • 修改消息显示区的头像。在 ShowMessageArea 的MessageItem::makeMessageItem 中处理changeAvatarDone 信号。和修改名字放在⼀起:
cpp 复制代码
// 6. 当用户修改了昵称的时候, 同步修改此处的用户昵称.
if(!isLeft)
{
	model::DataCenter* dataCenter = model::DataCenter::getInstance();
	connect(dataCenter, &model::DataCenter::changeNicknameDone, messageItem, [=]()
	{
		nameLabel->setText(dataCenter->getMyself()->nickname + " | " + message.time);
	});

	connect(dataCenter, &model::DataCenter::changeAvatarDone, messageItem, [=]()
	{
		UserInfo* myself = dataCenter->getMyself();
		avatarBtn->setIcon(myself->avatar);
	});
}

(3)服务器实现逻辑:

  • 注册路由:
cpp 复制代码
httpServer.route("/service/user/set_avatar", [=](const QHttpServerRequest& req) 
{
 	return this->setAvatar(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::setAvatar(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::SetUserAvatarReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 修改头像] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId();

    // 构造响应 body
    bite_im::SetUserAvatarRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");
    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

3. 用户详细信息界面逻辑

3.1 获取指定用户的信息

(1)客户端处理逻辑:

  • 从对应的 Message 对象中获取到用户详细信息。不需要从服务器获取数据。在 UserInfoWidget 的构造函数中,添加逻辑获取数据:
cpp 复制代码
// 9. 初始化按钮的禁用关系
    //    判定依据就是拿着当前用户的 userId, 在 DataCenter 的好友列表中, 查询即可.
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    auto* myFriend = dataCenter->findFriendById(this->userInfo.userId);
    if(myFriend == nullptr)
    {
        // 不是好友
        sendMessageBtn->setEnabled(false);
        deleteFriendBtn->setEnabled(false);
    }
    else
    {
        // 是好友
        applyBtn->setEnabled(false);
    }
  • 实现 DataCenter::findFriendById函数:
cpp 复制代码
UserInfo* DataCenter::findFriendById(const QString& userId)
{
    if(friendList == nullptr)
    {
        return nullptr;
    }

    for(auto& f : *friendList)
    {
        if(f.userId == userId)
        {
            return &f;
        }
    }

    return nullptr;
}

3.2 点击 "发送消息" 打开对应会话

(1)客户端处理逻辑:直接调用之前的逻辑即可。在选中好友时有类似的逻辑,直接调用即可。不需要和服务器交互:

  • 在 UserInfoWidget 构造函数中,连接信号槽:
cpp 复制代码
void UserInfoWidget::initSignalSlot()
{
    connect(sendMessageBtn, &QPushButton::clicked, this, [=]()
    {
        // 拿到主窗口指针, 通过主窗口中, 前面实现的 切换到会话 这样的功能, 直接调用即可.
        MainWidget* mainWidget = MainWidget::getInstance();
        mainWidget->switchSession(userInfo.userId);

        // 把本窗口关闭掉
        this->close();
    });
}

3.3 删除好友

(1)客户端发送请求:

  • 在 UserInfoWidget 构造函数中连接信号槽
cpp 复制代码
connect(deleteFriendBtn, &QPushButton::clicked, this, &UserInfoWidget::clickDeleteFriendBtn);
  • 实现 UserInfoWidget::clickDeleteFriendBtn函数:
cpp 复制代码
void UserInfoWidget::clickDeleteFriendBtn()
{
    // 1. 弹出对话框, 让用户确认是否要真的删除
    auto result = QMessageBox::warning(this, "确认删除", "确认删除当前好友?", QMessageBox::Ok | QMessageBox::Cancel);
    if (result != QMessageBox::Ok) {
        LOG() << "删除好友取消";
        return;
    }

    // 2. 发送网络请求, 实现删除好友功能.
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    dataCenter->deleteFriendAsync(userInfo.userId);

    // 3. 关闭本窗口
    this->close();
}
  • 实现 DataCenter::deleteFriendAsync函数:
cpp 复制代码
void DataCenter::deleteFriendAsync(const QString &userId)
{
 	netClient.deleteFriend(loginSessionId, userId);
}
  • 实现 NetClient::deleteFriend函数和接口定义:
cpp 复制代码
//--------------------------------------
//好友删除
message FriendRemoveReq {
 	string request_id = 1;
 	optional string user_id = 2;
 	optional string session_id = 3;
 	string peer_id = 4;
}
message FriendRemoveRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
}

// 函数实现
void NetClient::deleteFriend(const QString& loginSessionId, const QString& userId)
{
    // 1. 构造请求 body
    bite_im::FriendRemoveReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setSessionId(loginSessionId);
    pbReq.setPeerId(userId);
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[删除好友] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", peerId=" << pbReq.peerId();

    // 2. 发送 HTTP 请求
    QNetworkReply* resp = this->sendHttpRequest("/service/friend/remove_friend", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::FriendRemoveRsp>(resp, &ok, &reason);

        // b) 判定响应结果
        if(!ok)
        {
            LOG() << "[删除好友] 响应失败! reason=" << reason;
            return;
        }

        // c) 把结果写入 DataCenter. 把该删除的用户, 从好友列表中, 删除掉.
        dataCenter->removeFriend(userId);

        // d) 发送信号, 通知调用者当前好友删除完毕.
        emit dataCenter->deleteFriendDone();

        // e) 打印日志
        LOG() << "[删除好友] 响应完成 requestId=" << pbResp->requestId();
    });
}

(2)客户端处理响应:

  • 实现 DataCenter::removeFriend函数删除好友和会话:
cpp 复制代码
void DataCenter::removeFriend(const QString& userId)
{
    // 遍历 friendList, 删除其中匹配的元素即可.
    if(friendList == nullptr || chatSessionList == nullptr)
    {
        return;
    }

    friendList->removeIf([=](const UserInfo& userInfo)
    {
        // 返回 true 要删除的元素. false 直接跳过不删除.
        return userInfo.userId == userId;
    });

    // 还要考虑会话列表.
    // 没有好友, 保留会话, 后续往会话里发消息啥的, 就都不好处理了.
    // 删除会话操作, 客户端和服务器分别都会删除.
    chatSessionList->removeIf([=](const ChatSessionInfo& chatSessionInfo)
    {
        if(chatSessionInfo.userId == "")
        {
            // 群聊, 不受影响
            return false;
        }

        if (chatSessionInfo.userId == userId)
        {
            // 当前这个会话要删除了, 并且要删除的会话又是选中的会话, 才真正清空当前会话
            // 此处如果删除的会话, 正好是用户正在选中的会话, 此时就需要把当前选中会话的内容(标题和消息列表)都清空
            if(chatSessionInfo.chatSessionId == this->currentChatSessionId)
            {
                emit this->clearCurrentSession();
            }

            return true;
        }

        return false;
    });
}
  • 定义 DataCenter 信号:
cpp 复制代码
// 删除好友完成
void deleteFriendDone(const QString& userId);
void clearCurrentSession();
  • 处理 deleteFriendDone 信号和 clearCurrentSession 信号:
  • 在MainWidget::initData 中更新界面显示:
    • 更新会话列表。
    • 更新好友列表。
    • 更新当前会话的消息列表。
cpp 复制代码
/
/// 处理删除好友
/

connect(dataCenter, &DataCenter::deleteFriendDone, this, [=]()
{
	// 更新会话列表和好友列表
	this->updateFriendList();
	this->updateChatSessionList();
	LOG() << "删除好友完成";
});

connect(dataCenter, &DataCenter::clearCurrentSession, this, [=]()
{
	sessionTitleLabel->setText("");
	messageShowArea->clear();
	dataCenter->setCurrentChatSessionId("");
	LOG() << "清空当前会话完成";
});

(3)服务器实现逻辑:

  • 注册路由:
cpp 复制代码
httpServer.route("/service/friend/remove_friend", [=](const QHttpServerRequest& req) 
{
 	return this->removeFriend(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::removeFriend(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::FriendRemoveReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 删除好友] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", peerId=" << pbReq.peerId();

    // 构造响应 body
    bite_im::FriendRemoveRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");
    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

3.4 删除好友推送处理

当A删除B好友时,B也会收到⼀个websocket的推送信息。

(1)客户端处理推送:

  • 实现 NetClient::handleWsRemoveFriend函数。此处 deleteFriendDone 信号已经被处理过了:
cpp 复制代码
void NetClient::handleWsRemoveFriend(const QString& userId)
{
    // 1. 删除数据. DataCenter 好友列表的数据
    dataCenter->removeFriend(userId);
    // 2. 通知界面变化. 更新 好友列表 / 会话列表
    emit dataCenter->deleteFriendDone();
}

(2)服务器实现逻辑:

  • 在界面上添加按钮 "发送删除好友推送",并实现槽函数:
cpp 复制代码
void Widget::on_pushButton_9_clicked()
{
 	WebsocketServer* websocketServer = WebsocketServer::getInstance();
 	emit websocketServer->sendFriendRemove();
}
  • 定义 WebsocketServer 信号并处理:
cpp 复制代码
// 定义信号
void sendFriendRemove();

// 实现
connect(this, &WebsocketServer::sendFriendRemove, this, [=]()
{
	if(socket == nullptr || !socket->isValid())
	{
		LOG() << "socket 对象无效";
		return;
	}

	bite_im::NotifyMessage notifyMessage;
	notifyMessage.setNotifyEventId("");
	notifyMessage.setNotifyType(bite_im::NotifyTypeGadget::NotifyType::FRIEND_REMOVE_NOTIFY);

	bite_im::NotifyFriendRemove notifyFriendRemove;
	notifyFriendRemove.setUserId("1000");
	notifyMessage.setFriendRemove(notifyFriendRemove);

	QByteArray body = notifyMessage.serialize(&serializer);
	socket->sendBinaryMessage(body);
	LOG() << "通知对方好友被删除 userId=1000";
});
  • 断开连接时断开信号槽在 connect(socket, &QWebSocket::disconnected, this, [=] () {})中调用:
cpp 复制代码
disconnect(this, &WebsocketServer::sendFriendRemove, this, nullptr);

3.5 发送好友申请

点击 "申请好友按钮"触发该效果。

(1)客户端发送请求:

  • 在 UserInfoWidget 构造函数中连接信号槽:
cpp 复制代码
connect(addFriendBtn, &QPushButton::clicked, this, &UserInfoWidget::clickAddFriendBtn);
  • 实现 UserInfoWidget::clickAddFriendBtn函数:
cpp 复制代码
void UserInfoWidget::clickApplyBtn()
{
    // 1. 发送好友申请
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    dataCenter->addFriendApplyAsync(userInfo.userId);
    // 2. 关闭窗口
    this->close();
}
  • 实现 DataCenter::addFriendApplyAsync函数:
cpp 复制代码
void DataCenter::addFriendApplyAsync(const QString &userId)
{
 	netClient.addFriend(loginSessionId, userId);
}
  • 实现 NetClient::addFriendApply函数和接口定义:
cpp 复制代码
//添加好友--发送好友申请
message FriendAddReq {
 	string request_id = 1;
 	optional string session_id = 2;
 	optional string user_id = 3;//申请⼈id
 	string respondent_id = 4;//被申请⼈id
}
message FriendAddRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
 	string notify_event_id = 4;//通知事件id
}

// 函数实现
void NetClient::addFriendApply(const QString &loginSessionId, const QString &userId)
{
    // 1. 构造请求 body
    bite_im::FriendAddReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setSessionId(loginSessionId);
    pbReq.setRespondentId(userId);
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[添加好友申请] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", userId=" << userId;

    // 2. 发送 HTTP 请求
    QNetworkReply* resp = this->sendHttpRequest("/service/friend/add_friend_apply", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::FriendAddRsp>(resp, &ok, &reason);

        // b) 判定响应是否正确
        if(!ok)
        {
            LOG() << "[添加好友申请] 响应失败! reason=" << reason;
            return;
        }

        // c) 记录结果到 DataCenter, 此处不需要记录任何数据

        // d) 发送信号, 通知调用者
        emit dataCenter->addFriendApplyDone();

        // e) 打印日志
        LOG() << "[添加好友申请] 响应完毕 requestId=" << pbResp->requestId();
    });
}

(2)客户端处理响应:

  • 定义 DataCenter 信号:
cpp 复制代码
void addFriendApplyDone();
  • 在 MainWidget::initData 中, 处理 addFriendApplyDone 信号.
cpp 复制代码
connect(dataCenter, &DataCenter::addFriendApplyDone, this, [=]() 
{
 	Toast::showMessage("好友申请已发送!");
});

(3)服务器实现逻辑:

  • 注册路由
cpp 复制代码
httpServer.route("/service/friend/add_friend_apply", [=](const QHttpServerRequest& req) 
{
 	return this->addFriendApply(req);
});
  • 实现处理函数:
cpp 复制代码
QHttpServerResponse HttpServer::addFriendApply(const QHttpServerRequest& req)
{
    // 解析请求
    bite_im::FriendAddReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 添加好友申请] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", userId=" << pbReq.respondentId();

    // 构造响应 body
    bite_im::FriendAddRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");
    pbResp.setNotifyEventId("");

    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

4. 主界面逻辑 (2)

4.1 收到好友申请

(1)客户端处理推送:通过 websocket 收到 "好友申请通知",并进行处理

  • 实现 NetClient::handleWsAddFriendApplyReq函数:
cpp 复制代码
void NetClient::handleWsAddFriendApply(const model::UserInfo &userInfo)
{
    // 1. DataCenter 中有一个 好友申请列表. 需要把这个数据添加到好友申请列表中
    QList<model::UserInfo>* applyList = dataCenter->getApplyList();
    if(applyList == nullptr)
    {
        LOG() << "客户端没有加载到好友申请列表!";
        return;
    }

    // 把新的元素放到列表前面
    applyList->push_front(userInfo);

    // 2. 通知界面更新.
    emit dataCenter->receiveFriendApplyDone();
}
  • 实现 DataCenter::getApplyList函数:
cpp 复制代码
QList<UserInfo> *DataCenter::getApplyList()
{
 	return applyList;
}
  • 定义 DataCenter 信号:
cpp 复制代码
void receiveFriendApplyDone();
  • 在MainWidget::initSignalSlot中处理上述信号:
cpp 复制代码
connect(dataCenter, &DataCenter::receiveFriendApplyDone, this, [=]() {
 	Toast::showMessage("收到新的好友申请!");
 	// 如果当前选中的标签⻚正好是好友申请, 则更新申请列表
 	updateApplyList();
});

(2)服务器实现逻辑:

  • 在界面上创建⼀个按钮,"发送好友申请推送",并实现槽函数:
cpp 复制代码
void Widget::on_pushButton_2_clicked()
{
 	WebsocketServer* websocketServer = WebsocketServer::getInstance();
 	emit websocketServer->sendFriendApply();
}
  • 在 websocket 逻辑中处理上述信号:
cpp 复制代码
connect(this, &WebsocketServer::sendAddFriendApply, this, [=]()
{
	if(socket == nullptr || !socket->isValid())
	{
		LOG() << "socket 对象无效";
		return;
	}

	bite_im::NotifyMessage notifyMessage;
	notifyMessage.setNotifyEventId("");
	notifyMessage.setNotifyType(bite_im::NotifyTypeGadget::NotifyType::FRIEND_ADD_APPLY_NOTIFY);

	QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");
	bite_im::UserInfo userInfo = makeUserInfo(100, avatar);

	bite_im::NotifyFriendAddApply friendAddApply;
	friendAddApply.setUserInfo(userInfo);

	notifyMessage.setFriendAddApply(friendAddApply);
	QByteArray body = notifyMessage.serialize(&serializer);
	socket->sendBinaryMessage(body);
	LOG() << "通知对方好友申请数据";
});
  • 在断开 websocket 连接时断开上述信号槽:
cpp 复制代码
disconnect(this, &WebsocketServer::sendFriendApply, this, nullptr);

4.2 同意好友申请

点击 "同意" 按钮触发下列逻辑。

(1)客户端发送请求:

  • 在 ApplyItem::ApplyItem 的构造函数中连接信号槽:
cpp 复制代码
connect(acceptBtn, &QPushButton::clicked, this, &ApplyItem::acceptFriend);
  • 实现 ApplyItem::acceptFriend
cpp 复制代码
void ApplyItem::acceptFriendApply()
{
    // 发送网络请求, 告知服务器, 同意了.
    model::DataCenter* dataCenter = model::DataCenter::getInstance();

    // 同意谁的好友申请了.
    // 针对这个操作, 信号处理, 是需要更新好友列表以及好友申请列表. 直接在主窗口中处理更合适的.
    dataCenter->acceptFriendApplyAsync(this->userId);
}
  • 实现 DataCenter::acceptFriendApplyAsync
cpp 复制代码
void DataCenter::acceptFriendApplyAsync(const QString &userId)
{
 	netClient.acceptFriendApply(loginSessionId, userId);
}
  • 实现 NetClient::acceptFriendApply函数和接口定义:
cpp 复制代码
//好友申请的处理
message FriendAddProcessReq {
 	string request_id = 1;
 	string notify_event_id = 2;//通知事件id
 	bool agree = 3;//是否同意好友申请
 	string apply_user_id = 4; //申请⼈的⽤⼾id
 	optional string session_id = 5;
 	optional string user_id = 6;
}
// +++++++++++++++++++++++++++++++++
message FriendAddProcessRsp {
 	string request_id = 1;
 	bool success = 2;
 	string errmsg = 3; 
 	optional string new_session_id = 4; // 同意后会创建会话,向⽹关返回会话信息,⽤于通知双⽅会话的建⽴,这个字段客⼾端不需要关注
}

// 函数实现
void NetClient::acceptFriendApply(const QString &loginSessionId, const QString &userId)
{
    // 1. 构造请求 body
    bite_im::FriendAddProcessReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setSessionId(loginSessionId);
    pbReq.setAgree(true);
    pbReq.setApplyUserId(userId);
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[同意好友申请] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", userId=" << pbReq.applyUserId();

    // 2. 发送 HTTP 请求
    QNetworkReply* resp = this->sendHttpRequest("/service/friend/add_friend_process", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::FriendAddRsp>(resp, &ok, &reason);

        // b) 判定响应结果是否正确
        if(!ok)
        {
            LOG() << "[同意好友申请] 处理失败! reason=" << reason;
            return;
        }

        // c) 此处做一个好友列表的更新
        //    一个是把数据从好友申请列表中, 删除掉
        //    另一个是把好友申请列表中的这个数据添加到好友列表中.
        model::UserInfo applyUser = dataCenter->removeFromApplyList(userId);
        QList<model::UserInfo>* friendList = dataCenter->getFriendList();
        friendList->push_front(applyUser);

        // d) 发送信号, 通知界面进行更新
        emit dataCenter->acceptFriendApplyDone();

        // e) 打印日志
        LOG() << "[同意好友申请] 响应完成! requestId=" << pbResp->requestId();
    });
}

(2)客户端处理响应:

  • 实现 DataCenter::removeFromApplyList函数:
cpp 复制代码
UserInfo DataCenter::removeFromApplyList(const QString& userId)
{
    if(applyList == nullptr)
    {
        return UserInfo();
    }

    for(auto iter = applyList->begin(); iter != applyList->end(); ++iter)
    {
        if(iter->userId == userId)
        {
            // 复制以下这个要删除的对象. 以备进行返回.
            UserInfo toDelete = *iter;
            applyList->erase(iter);
            return toDelete;
        }
    }

    return UserInfo();
}
  • 定义 DataCenter 信号:
cpp 复制代码
// 发送同意好友申请完成
void acceptFriendApplyDone(const QString& userId, const QString& reason);
  • 处理acceptFriendApplyDone信号:
cpp 复制代码
connect(dataCenter, &DataCenter::acceptFriendApplyDone, this, [=]()
{
	this->updateApplyList();
	this->updateFriendList();
	Toast::showMessage("好友已经添加完成");
});

(3)服务器实现逻辑:

  • 注册路由
cpp 复制代码
httpServer.route("/service/friend/add_friend_process", [=](const QHttpServerRequest& req) 
{
 	return this->addFriendProcess(req);
});
  • 处理函数实现:
cpp 复制代码
QHttpServerResponse HttpServer::addFriendProcess(const QHttpServerRequest &req)
{
    // 解析请求
    bite_im::FriendAddProcessReq pbReq;
    pbReq.deserialize(&serializer, req.body());
    LOG() << "[REQ 添加好友申请处理] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", applyUserId=" << pbReq.applyUserId() << ", agree=" << pbReq.agree();

    // 构造响应 body
    bite_im::FriendAddProcessRsp pbResp;
    pbResp.setRequestId(pbReq.requestId());
    pbResp.setSuccess(true);
    pbResp.setErrmsg("");
    pbResp.setNewSessionId("");
    QByteArray body = pbResp.serialize(&serializer);

    // 构造 HTTP 响应
    QHttpServerResponse resp(body, QHttpServerResponse::StatusCode::Ok);
    resp.setHeader("Content-Type", "application/x-protobuf");
    return resp;
}

4.3 拒绝好友申请

(1)客户端发送请求:

  • 在 ApplyItem::ApplyItem 的构造函数中连接信号槽
cpp 复制代码
connect(rejectBtn, &QPushButton::clicked, this, &ApplyItem::rejectFriendApply);
  • 实现 ApplyItem::rejectFriendApply函数:
cpp 复制代码
void ApplyItem::rejectFriendApply()
{
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    dataCenter->rejectFriendApplyAsync(this->userId);
}
  • 实现 DataCenter::rejectFriendApplyAsync
cpp 复制代码
void DataCenter::rejectFriendApplyAsync(const QString &userId)
{
 	netClient.rejectFriendApply(loginSessionId, userId);
}
  • 实现 NetClient::rejectFriendApply并且接口定义 (和刚才的同意好友申请是同⼀套接口):
cpp 复制代码
void NetClient::rejectFriendApply(const QString &loginSessionId, const QString &userId)
{
    // 1. 构造请求 body
    bite_im::FriendAddProcessReq pbReq;
    pbReq.setRequestId(makeRequestId());
    pbReq.setSessionId(loginSessionId);
    pbReq.setAgree(false);
    pbReq.setApplyUserId(userId);
    QByteArray body = pbReq.serialize(&serializer);
    LOG() << "[拒绝好友申请] 发送请求 requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId()
          << ", userId=" << pbReq.applyUserId();

    // 2. 发送 HTTP 请求
    QNetworkReply* resp = this->sendHttpRequest("/service/friend/add_friend_process", body);

    // 3. 处理响应
    connect(resp, &QNetworkReply::finished, this, [=]()
    {
        // a) 解析响应
        bool ok = false;
        QString reason;
        auto pbResp = this->handleHttpResponse<bite_im::FriendAddRsp>(resp, &ok, &reason);

        // b) 判定响应结果是否正确
        if(!ok)
        {
            LOG() << "[拒绝好友申请] 处理失败! reason=" << reason;
            return;
        }

        // c) 此处不需要更新好友列表, 需要把这个记录从好友申请列表中, 删除掉.
        dataCenter->removeFromApplyList(userId);

        // d) 发送信号, 通知界面进行更新
        emit dataCenter->rejectFriendApplyDone();

        // e) 打印日志
        LOG() << "[拒绝好友申请] 响应完成! requestId=" << pbResp->requestId();
    });
}

(2)客户端处理响应:

  • 实现 DataCenter::removeFromApplyList函数。这个函数上面 "同意好友申请" 中已经实现过,直接调用即可
  • 定义 DataCenter 信号
cpp 复制代码
// 发送拒绝好友申请完成
void rejectFriendApplyDone(const QString& userId, const QString& reason);
  • 处理rejectFriendApplyDone信号:
cpp 复制代码
connect(dataCenter, &DataCenter::rejectFriendApplyDone, this, [=]()
{
	// 需要更新好友申请列表. 刚才拒绝的这一项, 是需要删除掉的.
	this->updateApplyList();
	Toast::showMessage("好友申请已经拒绝");
});

(3)服务器实现逻辑:此处的逻辑和 "同意好友申请" 的服务器逻辑是⼀致的。

4.4 获取到好友申请处理结果

服务器通过 websocket 推送数据。

(1)客户端处理推送:

  • 实现 NetClient::handleWsAddFriendProcess函数:
cpp 复制代码
void NetClient::handleWsAddFriendProcess(const model::UserInfo &userInfo, bool agree)
{
    if(agree)
    {
        // 对方同意了你的好友申请
        QList<model::UserInfo>* friendList = dataCenter->getFriendList();
        if(friendList == nullptr)
        {
            LOG() << "客户端没有加载好友列表";
            return;
        }

        friendList->push_front(userInfo);

        // 同时也更新一下界面
        emit dataCenter->receiveFriendProcessDone(userInfo.nickname, agree);
    }
    else
    {
        // 对方未同意好友申请
        emit dataCenter->receiveFriendProcessDone(userInfo.nickname, agree);
    }
}
  • 定义 DataCenter 信号:
cpp 复制代码
// 一个信号处理是否同意申请信息
void receiveFriendProcessDone(const QString& nickname, bool agree);
  • 在 MainWidget::initData 中处理上述信号:
cpp 复制代码
/
/// 处理好友申请结果的推送数据
/
connect(dataCenter, &DataCenter::receiveFriendProcessDone, this, [=](const QString& nickname, bool agree)
{
	if(agree)
	{
		// 同意
		this->updateFriendList();
		Toast::showMessage(nickname + " 已经同意了你的好友申请");
	}
	else
	{
		// 拒绝
		Toast::showMessage(nickname + " 已经拒绝了你的好友申请");
	}
});

(2)服务器实现逻辑:

  • 创建按钮"发送好友通过通知" 和 "发送好友拒绝通知"并创建槽函数:
cpp 复制代码
void Widget::on_pushButton_3_clicked()
{
 	WebsocketServer* websocketServer = WebsocketServer::getInstance();
 	emit websocketServer->sendFriendProcess(true);
}

void Widget::on_pushButton_4_clicked()
{
 	WebsocketServer* websocketServer = WebsocketServer::getInstance();
 	emit websocketServer->sendFriendProcess(false);
}
  • 定义上述信号:
cpp 复制代码
void sendFriendProcess(bool);
  • 在 websocket 逻辑中,处理上述信号:
cpp 复制代码
connect(this, &WebsocketServer::sendAddFriendProcess, this, [=](const bool agree)
{
	if(socket == nullptr || !socket->isValid())
	{
		LOG() << "socket 对象无效!";
		return;
	}

	bite_im::NotifyMessage notifyMessage;
	notifyMessage.setNotifyEventId("");
	notifyMessage.setNotifyType(bite_im::NotifyTypeGadget::NotifyType::FRIEND_ADD_PROCESS_NOTIFY);

	QByteArray avatar = loadFileToByteArray(":/resource/image/defaultAvatar.png");
	bite_im::UserInfo userInfo = makeUserInfo(100, avatar);

	bite_im::NotifyFriendAddProcess friendAddProcess;
	friendAddProcess.setUserInfo(userInfo);
	friendAddProcess.setAgree(agree);

	notifyMessage.setFriendProcessResult(friendAddProcess);

	QByteArray body = notifyMessage.serialize(&serializer);
	socket->sendBinaryMessage(body);
	LOG() << "通知好友申请的处理结果 userId=" << userInfo.userId() << ", agree=" << agree;
});
  • 在 websocket 断开连接的逻辑中,断开上述信号槽:
cpp 复制代码
disconnect(this, &WebsocketServer::sendFriendProcess, this, nullptr);

5. 小结

(1)以上的所有内容都是实现前后端交互接口,实现的格式基本上是一致的。

(2)剩下的前后端交互接口的实现见博客:https://blog.csdn.net/m0_65558082/article/details/143828479?spm=1001.2014.3001.5502。

客户端整体代码链接https://gitee.com/liu-yechi/new_code/tree/master/chat_system/client。

相关推荐
surfirst3 小时前
【微服务设计】从理论到实践:2PC(两阶段提交)与SAGA的全面比较与示例
微服务·架构·事务处理
小gpt&7 小时前
qt布局设置(1,2,4,6,8,9,12,16等布局)
开发语言·qt·命令模式
moxiaoran575310 小时前
搭建Spring gateway网关微服务
spring·微服务·gateway
mahuifa10 小时前
C++(Qt)软件调试---内存泄漏分析工具MTuner (25)
c++·qt·内存泄漏·软件调试·mtuner
葛狂的博客10 小时前
【Qt实现虚拟键盘】
开发语言·qt·计算机外设
yyqzjw10 小时前
【qt】控件
开发语言·qt
2401_8576176214 小时前
基于Spring Boot的电子商务平台架构
spring boot·后端·架构
银帅1833503097114 小时前
2012年下半年试题一:论基于架构的软件设计方法及应用
架构·系统架构·论文笔记
心灵彼岸-诗和远方14 小时前
高效协作:前后端合作规范与应对策略优化
java·架构·devops