目录
- 1. 聊天界面逻辑
- 1.1 发送消息
- 1.2 接收消息
- 2. 个人信息详情逻辑
- 2.1 加载个人信息
- 2.2 修改昵称
- 2.3 修改签名
- 2.4 修改电话 (1) - 发起短信验证码
- 2.5 修改电话 (2) - 修改电话逻辑
- 2.6 修改头像
- 3. 用户详细信息界面逻辑
- 3.1 获取指定用户的信息
- 3.2 点击 "发送消息" 打开对应会话
- 3.3 删除好友
- 3.4 删除好友推送处理
- 3.5 发送好友申请
- 4. 主界面逻辑 (2)
- 4.1 收到好友申请
- 4.2 同意好友申请
- 4.3 拒绝好友申请
- 4.4 获取到好友申请处理结果
- 5. 小结
1. 聊天界面逻辑
1.1 发送消息
(1)客户端发送消息请求:
- 在 MessageEditArea 中创建 initSignalSlot 方法。关联上 sendTextBtn的槽函数:
void MessageEditArea::initSignalSlot()
{DataCenter* dataCenter = DataCenter::getInstance();// 处理按钮点击connect(sendTextBtn, &QPushButton::clicked, this, &MessageEditArea::sendTextMessage);
}
- 实现 MessageEditArea::sendTextMessage函数:
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函数:
void DataCenter::sendTextMessageAsync(const QString& chatSessionId, const QString& content)
{netClient.sendMessage(loginSessionId, chatSessionId, MessageType::TEXT_TYPE, content.toUtf8(), "");
}
- 实现 NetClient::sendMessage函数以及接口定义:
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 构造 bodybite_im::NewMessageReq pbReq;pbReq.setRequestId(makeRequestId());pbReq.setSessionId(loginSessionId);pbReq.setChatSessionId(chatSessionId);// 构造 MessageContentbite_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 信号:
// 发送消息完成
void sendMessageDone(MessageType messageType, const QByteArray& content, constQString& extraInfo);
void sendMessageFailed(const QString& reason);
- 修改 MessageEditArea::initSignalSlot,新增信号槽连接:
// 处理发送消息的⽹络相应, 把⾃⼰发的内容添加到消息展⽰区
connect(dataCenter, &DataCenter::sendMessageDone, this, &MessageEditArea::addSelfMessage);
connect(dataCenter, &DataCenter::sendMessageFailed, this, [=](const QString& reason)
{Toast::showMessage("发送消息失败! " + reason);
});
- 新增函数 MessageEditArea::addSelfMessage函数将消息添加到消息显示区:
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 定义信号。更新会话列表中的 “最后⼀条消息”:
// 更新会话列表中的最后⼀条消息
void updateLastMessage(const QString& chatSessionId);
- 在 SessionArea 的 SessionItem 构造函数中,连接上述信号并处理:
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);}}
}
- 实现对于未读消息数据的处理:
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 中的未读消息处理:
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)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/message_transmit/new_message", [=](const QHttpServerRequest& req)
{return this->sendMessage(req);
});
- 实现处理函数:
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 收到的数据:
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函数:
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 信号:
// 收到消息
void receiveMessageDone(const Message& message);
- 修改 MessageEditArea::initSignalSlot,添加信号槽:
// 处理收到网络上来自别人的响应情况
connect(dataCenter, &DataCenter::receiveMessageDone, this, &MessageEditArea::addOtherMessage);
- 实现 MessageEditArea::addOtherMessage函数:
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)服务器实现逻辑:
- 在界面上创建⼀个按钮,表示 “发送文本消息”,并实现信号槽:
void Widget::on_pushButton_clicked()
{WebsocketServer* websocketServer = WebsocketServer::getInstance();emit websocketServer->sendTextResp();
}
- 给 WebsocketServer 创建信号 sendTextResp
signals:void sendTextResp();
- 实现处理函数:注意此处的 connect 要放到 connect(&websocketServer,
&QWebSocketServer::newConnection, this, [=] () { } ) 当中这样才能捕获到 socket 对象:
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 处理函数中,添加解除信号槽的逻辑:
// 针对这个 socket 对象, 进行剩余信号的处理
connect(socket, &QWebSocket::disconnected, this, [=]()
{qDebug() << "[websocket] 连接断开!";disconnect(this, &WebsocketServer::sendTextResp, this, nullptr);
}
此处的 disconnect 非常重要。否则如果客户端重复连接服务器,服务器就会尝试针对上次已经释放的socket 对象进行处理,就会使程序崩溃。
2. 个人信息详情逻辑
2.1 加载个人信息
(1)直接从 DataCenter 中读取数据:在 SelfInfoWidget 构造函数中, 添加数据加载:
// 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 构造函数连接信号槽并实现切换显示状态:
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函数:
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函数:
// 修改昵称
void DataCenter::changeNickNameAsync(const QString &nickName)
{netClient.changeNickName(loginSessionId, nickName);
}
- 实现 NetClient::changeNickName函数和接口定义:
//----------------------------
//⽤⼾昵称修改
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 构造请求 bodybite_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
void DataCenter::resetNickName(const QString& nickName)
{myself->nickname = nickName;
}
- 定义 DataCenter 信号:
void changeNickNameDone();
- 实现 SelfInfoWidget::clickNameSubmitBtnDone函数:
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,自动更新消息展示区的消息中显示的昵称:
// 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)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/set_nickname", [=](const QHttpServerRequest& req)
{return this->setNickName(req);
});
- 实现处理函数:
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 构造函数连接信号槽:
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函数:
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函数:
void DataCenter::changeDescriptionAsync(const QString &description)
{netClient.changeDescription(loginSessionId, description);
}
- 实现 NetClient::changeDescription和接口定义:
//----------------------------
//⽤⼾签名修改
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 构造请求 bodybite_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) 把得到的结果, 写入 DataCenterdataCenter->resetDescription(desc);// d) 发送信号, 通知修改完成emit dataCenter->changeDescriptionDone();// e) 打印日志LOG() << "[修改签名] 响应完成! requestId=" << pbResp->requestId();});
}
(2)客户端处理响应:
- 实现 DataCenter::resetDescription函数:
void DataCenter::resetDescription(const QString &description)
{myself->description = description;
}
- 定义 DataCenter 信号:
void changeDescriptionDone();
- 实现 SelfInfoWidget::chickDescSubmitBtnDone函数:
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)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/set_description", [=](const QHttpServerRequest& req)
{return this->setDesc(req);
});
- 实现处理函数:
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 构造函数连接信号槽:
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 中把发送验证码的手机号存起来,并在后续发送修改请求的时候使用这⼀个号码来请求。
- 确保重新绑定的手机号码和发送验证码的手机号码⼀致。(发送修改手机号请求的时候手机号码不能从输入框读取!)。
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函数:
void DataCenter::getVerifyCodeAsync(const QString& phone)
{netClient.getVerifyCode(phone);
}
- 实现 NetClient::getVerifyCode函数和接口定义:
//----------------------------
//⼿机号验证码获取
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 构造请求 bodybite_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) 保存数据到 DataCenterdataCenter->resetVerifyCodeId(pbResp->verifyCodeId());// d) 发送信号, 通知调用者emit dataCenter->getVerifyCodeDone();// e) 打印日志LOG() << "[获取手机验证码] 响应完成 requestId=" << pbResp->requestId();});
}
服务器会在 redis 中保存 verify_code_id 和 verify_code,给后续的验证提供支持。
(2)客户端处理响应:
- 实现 DataCenter::resetVerifyCodeId函数:
void DataCenter::resetVerifyCodeId(std::shared_ptr<bite_im::PhoneVerifyCodeRsp> resp)
{this->currentVerifyCodeId = resp->verifyCodeId();
}
- 定义 DataCenter 信号:
void getVerifyCodeDone();
这个信号暂时不使用。会在后续的 “手机号登录” 功能中使用。
(3)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/get_phone_verify_code", [=](const QHttpServerRequest& req)
{return this->getPhoneVerifyCode(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::getPhoneVerifyCode(const QHttpServerRequest& req)
{// 解析请求bite_im::PhoneVerifyCodeReq pbReq;pbReq.deserialize(&serializer, req.body());LOG() << "[REQ 获取短信验证码] requestId=" << pbReq.requestId() << ", phone=" << pbReq.phoneNumber();// 构造响应 bodybite_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 中把发送验证码的手机号存起来,并在后续发送修改请求的时候使用这⼀个号码来请求。
- 确保重新绑定的手机号码和发送验证码的手机号码⼀致。(发送修改⼿机号请求的时候手机号码不能从输入框读取!)。
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函数
void DataCenter::changePhoneAsync(const QString &phone, const QString& verifyCodeId, const QString& verifyCode)
{netClient.changePhone(loginSessionId, phone, verifyCodeId, verifyCode);
}
- 实现 NetClient::changePhone函数和接口定义:
//----------------------------
//⽤⼾⼿机修改
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 构造请求 bodybite_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函数:
void DataCenter::resetPhone(const QString &phone)
{myself->phone = phone;
}
- 定义 DataCenter 信号:
// 修改手机号完成
void changePhoneDone();
- 实现 SelfInfoWidget::clickPhoneSubmitBtnDone函数:
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)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/set_phone", [=](const QHttpServerRequest& req)
{return this->setPhone(req);
});
- 实现处理函数:
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();// 构造响应 bodybite_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 构造函数连接信号槽:
connect(avatarBtn, &QPushButton::clicked, this, &SelfInfoWidget::clickAvatarBtn);
}
- 实现 SelfInfoWidget::clickAvatar函数:
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函数:
void DataCenter::changeAvatarAsync(const QByteArray &avatar)
{netClient.changeAvatar(loginSessionId, avatar);
}
- 实现 NetClient::changeAvatar函数和接口定义:
//----------------------------
//⽤⼾头像修改
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 构造请求 bodybite_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函数:
void DataCenter::resetAvatar(const QByteArray &avatar)
{myself->avatar = makeIcon(avatar);
}
- 定义 DataCenter 信号:
void changeAvatarDone();
- 实现 SelfInfoWidget::clickAvatarDone函数来修改用户详情界面的头像:
void SelfInfoWidget::clickAvatarBtnDone()
{// 把设置的头像, 更新到界面上.model::DataCenter* dataCenter = model::DataCenter::getInstance();avatarBtn->setIcon(dataCenter->getMyself()->avatar);
}
- 修改主界面的头像。在 MainWidget::initData 中处理 changeAvatarDone 信号:
connect(dataCenter, &DataCenter::changeAvatarDone, this, [=]()
{UserInfo* myself = dataCenter->getMyself();userAvatar->setIcon(myself->avatar);
});
- 修改消息显示区的头像。在 ShowMessageArea 的MessageItem::makeMessageItem 中处理changeAvatarDone 信号。和修改名字放在⼀起:
// 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)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/user/set_avatar", [=](const QHttpServerRequest& req)
{return this->setAvatar(req);
});
- 实现处理函数:
QHttpServerResponse HttpServer::setAvatar(const QHttpServerRequest& req)
{// 解析请求bite_im::SetUserAvatarReq pbReq;pbReq.deserialize(&serializer, req.body());LOG() << "[REQ 修改头像] requestId=" << pbReq.requestId() << ", loginSessionId=" << pbReq.sessionId();// 构造响应 bodybite_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 的构造函数中,添加逻辑获取数据:
// 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函数:
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 构造函数中,连接信号槽:
void UserInfoWidget::initSignalSlot()
{connect(sendMessageBtn, &QPushButton::clicked, this, [=](){// 拿到主窗口指针, 通过主窗口中, 前面实现的 切换到会话 这样的功能, 直接调用即可.MainWidget* mainWidget = MainWidget::getInstance();mainWidget->switchSession(userInfo.userId);// 把本窗口关闭掉this->close();});
}
3.3 删除好友
(1)客户端发送请求:
- 在 UserInfoWidget 构造函数中连接信号槽
connect(deleteFriendBtn, &QPushButton::clicked, this, &UserInfoWidget::clickDeleteFriendBtn);
- 实现 UserInfoWidget::clickDeleteFriendBtn函数:
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函数:
void DataCenter::deleteFriendAsync(const QString &userId)
{netClient.deleteFriend(loginSessionId, userId);
}
- 实现 NetClient::deleteFriend函数和接口定义:
//--------------------------------------
//好友删除
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. 构造请求 bodybite_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函数删除好友和会话:
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 信号:
// 删除好友完成
void deleteFriendDone(const QString& userId);
void clearCurrentSession();
- 处理 deleteFriendDone 信号和 clearCurrentSession 信号:
- 在MainWidget::initData 中更新界面显示:
- 更新会话列表。
- 更新好友列表。
- 更新当前会话的消息列表。
/
/// 处理删除好友
/connect(dataCenter, &DataCenter::deleteFriendDone, this, [=]()
{// 更新会话列表和好友列表this->updateFriendList();this->updateChatSessionList();LOG() << "删除好友完成";
});connect(dataCenter, &DataCenter::clearCurrentSession, this, [=]()
{sessionTitleLabel->setText("");messageShowArea->clear();dataCenter->setCurrentChatSessionId("");LOG() << "清空当前会话完成";
});
(3)服务器实现逻辑:
- 注册路由:
httpServer.route("/service/friend/remove_friend", [=](const QHttpServerRequest& req)
{return this->removeFriend(req);
});
- 实现处理函数:
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();// 构造响应 bodybite_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 信号已经被处理过了:
void NetClient::handleWsRemoveFriend(const QString& userId)
{// 1. 删除数据. DataCenter 好友列表的数据dataCenter->removeFriend(userId);// 2. 通知界面变化. 更新 好友列表 / 会话列表emit dataCenter->deleteFriendDone();
}
(2)服务器实现逻辑:
- 在界面上添加按钮 “发送删除好友推送”,并实现槽函数:
void Widget::on_pushButton_9_clicked()
{WebsocketServer* websocketServer = WebsocketServer::getInstance();emit websocketServer->sendFriendRemove();
}
- 定义 WebsocketServer 信号并处理:
// 定义信号
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, [=] () {})中调用:
disconnect(this, &WebsocketServer::sendFriendRemove, this, nullptr);
3.5 发送好友申请
点击 "申请好友按钮"触发该效果。
(1)客户端发送请求:
- 在 UserInfoWidget 构造函数中连接信号槽:
connect(addFriendBtn, &QPushButton::clicked, this, &UserInfoWidget::clickAddFriendBtn);
- 实现 UserInfoWidget::clickAddFriendBtn函数:
void UserInfoWidget::clickApplyBtn()
{// 1. 发送好友申请model::DataCenter* dataCenter = model::DataCenter::getInstance();dataCenter->addFriendApplyAsync(userInfo.userId);// 2. 关闭窗口this->close();
}
- 实现 DataCenter::addFriendApplyAsync函数:
void DataCenter::addFriendApplyAsync(const QString &userId)
{netClient.addFriend(loginSessionId, userId);
}
- 实现 NetClient::addFriendApply函数和接口定义:
//添加好友--发送好友申请
message FriendAddReq {string request_id = 1;optional string session_id = 2;optional string user_id = 3;//申请⼈idstring 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. 构造请求 bodybite_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 信号:
void addFriendApplyDone();
- 在 MainWidget::initData 中, 处理 addFriendApplyDone 信号.
connect(dataCenter, &DataCenter::addFriendApplyDone, this, [=]()
{Toast::showMessage("好友申请已发送!");
});
(3)服务器实现逻辑:
- 注册路由
httpServer.route("/service/friend/add_friend_apply", [=](const QHttpServerRequest& req)
{return this->addFriendApply(req);
});
- 实现处理函数:
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();// 构造响应 bodybite_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函数:
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函数:
QList<UserInfo> *DataCenter::getApplyList()
{return applyList;
}
- 定义 DataCenter 信号:
void receiveFriendApplyDone();
- 在MainWidget::initSignalSlot中处理上述信号:
connect(dataCenter, &DataCenter::receiveFriendApplyDone, this, [=]() {Toast::showMessage("收到新的好友申请!");// 如果当前选中的标签⻚正好是好友申请, 则更新申请列表updateApplyList();
});
(2)服务器实现逻辑:
- 在界面上创建⼀个按钮,“发送好友申请推送”,并实现槽函数:
void Widget::on_pushButton_2_clicked()
{WebsocketServer* websocketServer = WebsocketServer::getInstance();emit websocketServer->sendFriendApply();
}
- 在 websocket 逻辑中处理上述信号:
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 连接时断开上述信号槽:
disconnect(this, &WebsocketServer::sendFriendApply, this, nullptr);
4.2 同意好友申请
点击 “同意” 按钮触发下列逻辑。
(1)客户端发送请求:
- 在 ApplyItem::ApplyItem 的构造函数中连接信号槽:
connect(acceptBtn, &QPushButton::clicked, this, &ApplyItem::acceptFriend);
- 实现 ApplyItem::acceptFriend
void ApplyItem::acceptFriendApply()
{// 发送网络请求, 告知服务器, 同意了.model::DataCenter* dataCenter = model::DataCenter::getInstance();// 同意谁的好友申请了.// 针对这个操作, 信号处理, 是需要更新好友列表以及好友申请列表. 直接在主窗口中处理更合适的.dataCenter->acceptFriendApplyAsync(this->userId);
}
- 实现 DataCenter::acceptFriendApplyAsync
void DataCenter::acceptFriendApplyAsync(const QString &userId)
{netClient.acceptFriendApply(loginSessionId, userId);
}
- 实现 NetClient::acceptFriendApply函数和接口定义:
//好友申请的处理
message FriendAddProcessReq {string request_id = 1;string notify_event_id = 2;//通知事件idbool agree = 3;//是否同意好友申请string apply_user_id = 4; //申请⼈的⽤⼾idoptional 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. 构造请求 bodybite_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函数:
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 信号:
// 发送同意好友申请完成
void acceptFriendApplyDone(const QString& userId, const QString& reason);
- 处理acceptFriendApplyDone信号:
connect(dataCenter, &DataCenter::acceptFriendApplyDone, this, [=]()
{this->updateApplyList();this->updateFriendList();Toast::showMessage("好友已经添加完成");
});
(3)服务器实现逻辑:
- 注册路由
httpServer.route("/service/friend/add_friend_process", [=](const QHttpServerRequest& req)
{return this->addFriendProcess(req);
});
- 处理函数实现:
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();// 构造响应 bodybite_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 的构造函数中连接信号槽
connect(rejectBtn, &QPushButton::clicked, this, &ApplyItem::rejectFriendApply);
- 实现 ApplyItem::rejectFriendApply函数:
void ApplyItem::rejectFriendApply()
{model::DataCenter* dataCenter = model::DataCenter::getInstance();dataCenter->rejectFriendApplyAsync(this->userId);
}
- 实现 DataCenter::rejectFriendApplyAsync
void DataCenter::rejectFriendApplyAsync(const QString &userId)
{netClient.rejectFriendApply(loginSessionId, userId);
}
- 实现 NetClient::rejectFriendApply并且接口定义 (和刚才的同意好友申请是同⼀套接口):
void NetClient::rejectFriendApply(const QString &loginSessionId, const QString &userId)
{// 1. 构造请求 bodybite_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 信号
// 发送拒绝好友申请完成
void rejectFriendApplyDone(const QString& userId, const QString& reason);
- 处理rejectFriendApplyDone信号:
connect(dataCenter, &DataCenter::rejectFriendApplyDone, this, [=]()
{// 需要更新好友申请列表. 刚才拒绝的这一项, 是需要删除掉的.this->updateApplyList();Toast::showMessage("好友申请已经拒绝");
});
(3)服务器实现逻辑:此处的逻辑和 “同意好友申请” 的服务器逻辑是⼀致的。
4.4 获取到好友申请处理结果
服务器通过 websocket 推送数据。
(1)客户端处理推送:
- 实现 NetClient::handleWsAddFriendProcess函数:
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 信号:
// 一个信号处理是否同意申请信息
void receiveFriendProcessDone(const QString& nickname, bool agree);
- 在 MainWidget::initData 中处理上述信号:
/
/// 处理好友申请结果的推送数据
/
connect(dataCenter, &DataCenter::receiveFriendProcessDone, this, [=](const QString& nickname, bool agree)
{if(agree){// 同意this->updateFriendList();Toast::showMessage(nickname + " 已经同意了你的好友申请");}else{// 拒绝Toast::showMessage(nickname + " 已经拒绝了你的好友申请");}
});
(2)服务器实现逻辑:
- 创建按钮"发送好友通过通知" 和 "发送好友拒绝通知"并创建槽函数:
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);
}
- 定义上述信号:
void sendFriendProcess(bool);
- 在 websocket 逻辑中,处理上述信号:
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 断开连接的逻辑中,断开上述信号槽:
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。