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

目录

  • 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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/473433.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

文心一言编写小球反弹程序并优化

使用文心一言尝试编写一个小游戏&#xff0c;先完成 1.python中用pygame模块设计出一个显示区域720x540的屏幕&#xff0c;并绘制一个小球&#xff0c;可以完成小球在显示区域内自动随机直线移动&#xff0c;碰到显示区域的便捷并反弹 import pygame import random import sy…

华为开源自研AI框架昇思MindSpore应用案例:人体关键点检测模型Lite-HRNet

如果你对MindSpore感兴趣&#xff0c;可以关注昇思MindSpore社区 一、环境准备 1.进入ModelArts官网 云平台帮助用户快速创建和部署模型&#xff0c;管理全周期AI工作流&#xff0c;选择下面的云平台以开始使用昇思MindSpore&#xff0c;获取安装命令&#xff0c;安装MindSpo…

gitlab和jenkins连接

一&#xff1a;jenkins 配置 安装gitlab插件 生成密钥 id_rsa 要上传到jenkins&#xff0c;id_rsa.pub要上传到gitlab cat /root/.ssh/id_rsa 复制查看的内容 可以看到已经成功创建出来了对于gitlab的认证凭据 二&#xff1a;配置gitlab cat /root/.ssh/id_rsa.pub 复制查…

SpringBoot实现WebSocket

参考链接&#xff1a;https://www.kancloud.cn/king_om/mic_03/2783864 一、环境搭建 1.创建SpringBoot项目&#xff0c;引入相关依赖 <dependencies><!-- Spring Boot核心启动器&#xff0c;引入常用依赖基础 --><dependency><groupId>org.springf…

现代密码学|公钥密码体制 | RSA加密算法及其数学基础

文章目录 公钥密码RSA数学基础欧拉函数欧拉定理模指数运算 RSA加密算法对rsa的攻击 公钥密码 现代密码学&#xff5c;公钥密码体制概述 加密 A用B的公钥加密 B用B的私钥解密 认证 A使用A的私钥加密 B使用A的公钥解密 加密认证 A用A的私钥加密&#xff0c;再用B的公钥加密 B用…

VuePress v2 快速搭建属于自己的个人博客网站

目录 为什么用VuePress&#xff1f; 一、前期准备 Node.js 使用主题快速开发 二、VuePress安装 三、个性化定制 修改配置信息 删除不需要的信息 博客上传 四、部署 使用github快速部署 初始化仓库 本地配置 配置github的ssh密钥 部署 为什么用VuePress&#xff…

【阅读记录-章节1】Build a Large Language Model (From Scratch)

目录 1. Understanding large language models1.1 What is an LLM?补充介绍人工智能、机器学习和深度学习的关系机器学习 vs 深度学习传统机器学习 vs 深度学习&#xff08;以垃圾邮件分类为例&#xff09; 1.2 Applications of LLMs1.3 Stages of building and using LLMs1.4…

平台整合是网络安全成功的关键

如今&#xff0c;组织面临着日益复杂、动态的网络威胁环境&#xff0c;随着恶意行为者采用越来越阴险的技术来破坏环境&#xff0c;攻击的数量和有效性也在不断上升。我们最近的 Cyber​​Ark 身份威胁形势报告&#xff08;2024 年 5 月&#xff09;发现&#xff0c;去年 99% 的…

PlantUML——时序图

PlantUML时序图 背景 时序图&#xff08;Sequence Diagram&#xff09;&#xff0c;又名序列图、循序图&#xff0c;是一种UML交互图&#xff0c;用于描述对象之间发送消息的时间顺序&#xff0c;显示多个对象之间的动态协作。时序图的使用场景非常广泛&#xff0c;几乎各行各…

【MYSQL】分库分表

一、什么是分库分表 分库分表就是指在一个数据库在存储数据过大&#xff0c;或者一个表存储数据过多的情况下&#xff0c;为了提高数据存储的可持续性&#xff0c;查询数据的性能而进行的将单一库或者表分成多个库&#xff0c;表使用。 二、为什么要分库分表 分库分表其实是两…

Spring纯注解开发

在我的另一篇文章中&#xff08;初识Spring-CSDN博客&#xff09;&#xff0c;讲述了Bean&#xff0c;以及通过xml方式定义Bean。接下来将讲解通过注解的方法管理Bean。 我们在创建具体的类的时候&#xff0c;可以直接在类的上面标明“注解”&#xff0c;以此来声明类。 1. 常…

git push时报错! [rejected] master -> master (fetch first)error: ...

错误描述&#xff1a;在我向远程仓库push代码时&#xff0c;即执行 git push origin master命令时发生的错误。直接上错误截图。 错误截图 错误原因&#xff1a; 在网上查了许多资料&#xff0c;是因为Git仓库中已经有一部分代码&#xff0c;它不允许你直接把你的代码覆盖上去…

java常用工具包介绍

Java 作为一种广泛使用的编程语言&#xff0c;提供了丰富的标准库和工具包来帮助开发者高效地进行开发。这些工具包涵盖了从基础的数据类型操作到高级的网络编程、数据库连接等各个方面。下面是一些 Java 中常用的工具包&#xff08;Package&#xff09;及其简要介绍&#xff1…

latex中,两个相邻的表格,怎样留一定的空白

目录 问题描述 问题解决 问题描述 在使用latex写论文时&#xff0c;经常表格需要置顶写&#xff0c;则会出现两个表格连在一起的情况。下一个表名容易与上面的横线相连&#xff0c;如何通过明令&#xff0c;留出一定的空白。 问题解决 在第二个表格的 \centering命令之后…

react中如何在一张图片上加一个灰色蒙层,并添加事件?

最终效果&#xff1a; 实现原理&#xff1a; 移动到图片上的时候&#xff0c;给img加一个伪类 &#xff01;&#xff01;此时就要地方要注意了&#xff0c;因为img标签是闭合的标签&#xff0c;无法直接添加 伪类&#xff08;::after&#xff09;&#xff0c;所以 我是在img外…

C++builder中的人工智能(27):如何将 GPT-3 API 集成到 C++ 中

人工智能软件和硬件技术正在迅速发展。我们每天都能看到新的进步。其中一个巨大的飞跃是我们拥有更多基于自然语言处理&#xff08;NLP&#xff09;和深度学习&#xff08;DL&#xff09;机制的逻辑性更强的AI聊天应用。有许多AI工具可以用来开发由C、C、Delphi、Python等编程语…

【项目开发】URL中井号(#)的技术细节

未经许可,不得转载。 文章目录 前言一、# 的基本含义二、# 不参与 HTTP 请求三、# 后的字符处理机制四、# 的变化不会触发网页重新加载五、# 的变化会记录在浏览器历史中六、通过 window.location.hash 操作七、onhashchange 事件八、Google 对 # 的处理机制前言 2023 年 9 月…

AUTOSAR_EXP_ARAComAPI的7章笔记(5)

☞返回总目录 相关总结&#xff1a;典型的 SOME/IP 多绑定用例总结 7.3.3 典型的SOME/IP多绑定用例 在前面的章节中&#xff0c;我们简要提到&#xff0c;在一个典型的SOME/IP 网络协议的部署场景中&#xff0c;AP SWC不太可能自己打开套接字连接来与远程服务通信。为什么不…

Jenkins下载安装、构建部署到linux远程启动运行

Jenkins详细教程 Winodws下载安装Jenkins一、Jenkins配置Plugins插件管理1、汉化插件2、Maven插件3、重启Jenkins&#xff1a;Restart Safely插件4、文件传输&#xff1a;Publish Over SSH5、gitee插件6、清理插件&#xff1a;workspace cleanup system系统配置1、Gitee配置2、…

Flutter:Dio下载文件到本地

import dart:io; import package:dio/dio.dart;main(){// 创建dio对象final dio Dio();// 下载地址var url https://*******.org/files/1.0.0.apk;// 手机端路径String savePath Directory.systemTemp.path/ceshi.apk;print(savePath);downLoad(dio,url,savePath); }downLo…