写这部分代码的时候碰到了两次bug,耗时很久就解决了一个,所以才鸽了一个月再来更新。这里先提前说一下本文结束时代码的bug。当然不是代码的问题经排查,是ffmpeg我最开时时版本使用的有问题。导致mpv通过url播放远端服务器上的m3u8文件时不能通过调整进度条正确设置播放位置。为什么这里不解决了呢,是因为这个问题仅出现在本地环回测试上,而博主在写服务端时搁服务端弄了个m3u8文件让我这个“有问题”的客户端去播放,发现可以正常播放了。所以如果读者发现本地测试无法正常播放,不用担心,等我们服务端也写好之后一联调就没有这问题了。ok,废话不多说,我们继续客户端代码的编写。
还需要说明的一点是,因为我目前的代码量已经比上篇文章多很多了,我不会再像之前那样给出每一步的具体实现代码,因为每个人的大致逻辑虽然相同,但细节实现上大有不同,给出我自己细节部分的实现反而会造成干扰。所以只给出关键部分和主要逻辑在文章中,如有更详细的代码需要可以参考我前文给的源码连接。

一.播放m3u8文件

__前⾯实现视频播放时,播放的视频是从本地加载的,正常情况下,当⽤⼾点击⾸⻚的视频封⾯或标题时,应该从服务器获取要播放的视频⽂件,然后交给mpv库进⾏播放。要想播放服务器上视频⽂件,实现⼀个从服务器获取视频⽂件的接口即可。但问题是,⼀个视频⽂件可能⽐较⼤,如果直接从服务器获取完成视频⽂件后再播放,则客⼾端等待的时间太⻓了,可以尝试将原视频进⾏分⽚进⾏传输播放。
 m3u8是⼀种⽤于流媒体播放的索引⽂件格式,包含了多个媒体⽂件的路径或URL,本质上是基于⽂本的播放列表,⽤于指定视频或⾳频的分段⽂件以及播放顺序,使播放器能够按需加载和播放内容,是苹果公司为HLS(HTTP Live Streaming 流媒体⽹络传输协议)流媒体设计的标准格式,⼴泛⽤于在线视频和直播场景。
 当⽤⼾将视频⽂件上传服务器后,服务器会将视频⽂件切割成多个⼩⽂件(通常是.ts后缀的分⽚),M3U8⽂件记录这些分⽚的URL和顺序,播放器拿到M3U8⽂件后,根据内部管理的视频URL,下载并拼接播放,⼤⼤提升了流畅性。注意:mpv库⽀持M3U8⽂件播放。
 ffmpeg大家可以自己去网上搜索教程进行下载安装,如果不想安装可以用我在gitee中提交的样例视频。
 因为mpv播放器支持直接通过url播放远端服务器上的m3u8文件。所以客户端我们只需要改变startplay方法即可。
我们这里规定下请求的url格式:

GET /HttpService/downloadVideo?fileId=xxx
//body参数
application/octet-stream
//playerpage.cpp
void PlayerPage::startPlayer()
{
    //如果说弹幕框没有被初始化,初始化弹幕框
    if(bulletScreen == nullptr)
        initBulletScreen();

    bulletScreen->show();
    if(player == nullptr)
    {
        //用户点击播放时再去实例化player对象
        player = new MpvPlayer(ui->screen);
        //进度条同步播放时间
        connect(player,&MpvPlayer::timePosChanged,this,&PlayerPage::onTimePosChanged);
        //绑定所有视频分片播放完毕发出的信号
        connect(player,&MpvPlayer::endOfPlaylist,this,&PlayerPage::startPlayer);
        //设置默认音量为50%
        player->setVolume(50.0);
    }
    auto datacenter = model::DataCenter::getInstance();
    QString videoPath = datacenter->getHttpUrl() + "/HttpService/downloadVideo?fileId=" + videoInfo.videoFileId;
    player->startPlay(videoPath);
}

我们在本地服务端的视频文件摆放路径如下:
在这里插入图片描述
所以假服务端需要新增两个路由:

//mockServer.cpp
QHttpServerResponse MockServer::downloadVideo(const QHttpServerRequest &request)
{
    // 解析查询字符串
    QUrlQuery query(request.url());
    QString fileId = query.queryItemValue("fileId");
    LOG() << "[downloadVideo] 收到 downloadVideo 请求, videoFileId=" << fileId;
    // 构造m3u8文件路径
    QDir dir(QDir::currentPath());
    dir.cdUp();
    dir.cdUp();
    QString videoPath = dir.absolutePath();
    videoPath += idPathTable[fileId.toInt()];
    LOG()<<"视频ID:"<<fileId<<"--"<<videoPath;
    // 读取视频m3u8文件数据
    QByteArray videoData = loadFileToByteArray(videoPath);
    // 构造 HTTP 响应
    QHttpServerResponse httpResp(videoData,QHttpServerResponse::StatusCode::Ok);
    httpResp.setHeader("Content-Type", "application/octet-stream");
    return httpResp;
}

QHttpServerResponse MockServer::downloadVideoSegmentation(const QString& fileName)
{
    // 构造视频文件的路径
    QDir dir(QDir::currentPath());
    dir.cdUp();
    dir.cdUp();
    QString videoPath = dir.absolutePath();
    videoPath += "/videos/" + fileName;

    LOG() << videoPath;

    // 读取视频分片数据
    QByteArray videoData = loadFileToByteArray(videoPath);

    // 构造HTTP响应并返回给客户端
    QHttpServerResponse httpResp(videoData, QHttpServerResponse::StatusCode::Ok);
    httpResp.setHeader("Content-Type", "application/octet-stream");
    return httpResp;
}

而我们之前播放视频时是通过playTime去检测是否播放完毕的,不够严谨,所以我们这里需要通过mpv去获取播放时间,同时根据当前播放的是那个分片与总分片个数进行对比来判断视频是否播放完毕:

//mpvplayer.cpp
void MpvPlayer::handleMpvEvent(mpv_event *event)
{
    switch (event->event_id) {
    case MPV_EVENT_PROPERTY_CHANGE:
    {
        mpv_event_property* eventPropery = (mpv_event_property*)event->data;
        if(eventPropery->data == nullptr)
        {
            //判断数据是否为空-因为程序刚启动时不一定立马就有视频播放
            return;
        }
        //判断事件是否为timePos
        if (strcmp(eventPropery->name, "time-pos") == 0) {
            // 获取当前分片的起始时间
            double segmentStartTime = 0;
            mpv_get_property(mpv, "demuxer-start-time", MPV_FORMAT_DOUBLE, &segmentStartTime);
            // 获取当前分片内的播放时间
            double segmentCurrentTime = 0;
            mpv_get_property(mpv, "time-pos", MPV_FORMAT_DOUBLE, &segmentCurrentTime);
            currentTime = (int64_t)(segmentStartTime + segmentCurrentTime);
            emit timePosChanged(currentTime);
        }
        break;
    }
    case MPV_EVENT_END_FILE:
    {
        mpv_event_end_file* endFile = (mpv_event_end_file*)event->data;
        if(endFile->reason == MPV_END_FILE_REASON_EOF){
            // 检查是否播放最后⼀个视频分⽚
            int64_t playlist_pos = -1;
            int64_t playlist_count = -1;
            mpv_get_property(mpv, "playlist-pos", MPV_FORMAT_INT64,&playlist_pos);
            mpv_get_property(mpv, "playlist-count", MPV_FORMAT_INT64, &playlist_count);
            LOG() << playlist_pos << ":" <<playlist_count;
            // 综合判断条件
            if(((playlist_count > 0) && (playlist_pos == playlist_count - 1)) || playlist_pos == -1)
            {
                LOG() << "所有视频分片播放完毕";
                emit endOfPlaylist();
            }
            else
            {
                LOG() << "单个分片播放结束";
            }
        }
        break;
    }
    case MPV_EVENT_SHUTDOWN:
    {
        //如果是mpv关闭事件则释放mpv对象
        if(mpv)
        {
            mpv_terminate_destroy(mpv);
            mpv = nullptr;
        }
        break;
    }
    default:
        break;
    }
}

上面代码中在mpvPlayer类中新增currentTime 成员变量。剩余部分与playTime相关的修改部分不再给出。

二.下载弹幕

视频在播放时,需要同步加载各播放时间的弹幕数据,并显示在视频界⾯上。弹幕数据存储在服务器上,在视频播放前获取到弹幕数据后,再播放视频。
获取弹幕的接口定义如下:
请求URL

POST /HttpService/getBarrage 

请求参数:

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
videoId string 视频ID

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
result object 返回结果
barrageList array 弹幕列表
barrageId string 弹幕ID
userId string 发送用户
barrageContent string 弹幕内容
barrageTime integer 弹幕时间-相对秒数

接下来就是经典步骤了dataCenter定义异步方法与完成信号,服务端接收请求返回响应。

//dataCenter.h
    //下载视频的弹幕数据
    void downloadVideoBarrage(const QString& videoId);
  signals:
    //下载弹幕数据请求成功响应
    void downloadVideoBarrageDone(const QString& videoId,const QJsonObject& barrages);
//datacenter.cpp
void DataCenter::downloadVideoBarrage(const QString &videoId)
{
    netClient.downloadVideoBarrage(videoId);
}

//netclient.h
void downloadVideoBarrage(const QString &videoId);//下载视频的弹幕数据的请求
//netclient.cpp
void NetClient::downloadVideoBarrage(const QString &videoId)
{
    /*
    {
        "requestId": "string",
        "sessionId": "string",
        "videoId": "string"
    }
    */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["videoId"] = videoId;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/getBarrage",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        QJsonObject result = respBody["result"].toObject();
        emit dataCenter->downloadVideoBarrageDone(videoId,result);
    });
}

//mockserver.h
QHttpServerResponse downloadVideoBarrage(const QHttpServerRequest &request);//下载视频的弹幕数据的接口
QJsonArray bulidBarrages();//构造弹幕假数据的接口
//mockserver.cpp
QJsonArray MockServer::bulidBarrages()
{
    //只构造前100s的弹幕数据
    int totalTime = 100;
    QJsonArray barrageLists;
    for(int i = 0;i < 100;i++)
    {
        QJsonObject barrage;
        barrage["barrageId"] = QString::number(i);
        barrage["userId"] = "2";
        barrage["barrageContent"] = QString("测试弹幕-%1").arg(i);
        barrage["barrageTime"] = i;
        barrageLists.append(barrage);
    }
    return barrageLists;
}

QHttpServerResponse MockServer::downloadVideoBarrage(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[downloadVideoBarrage] 收到 downloadVideoBarrage 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    QJsonObject result;
    result["barrageList"] = bulidBarrages();
    respData["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

那么当客户端成功收到服务端传过来的json报文后,则需要进行解析并加载到当前播放器的弹幕列表中。同时我们需要在弹幕数据加载完毕之后再去播放视频,所以添加新函数去处理响应信号的同时还需要将原来的播放逻辑修改为一下形式:

//playerpage.cpp
void PlayerPage::startPlayer()
{
    //如果说弹幕框没有被初始化,初始化弹幕框
    if(bulletScreen == nullptr)
        initBulletScreen();

    bulletScreen->show();
    if(player == nullptr)
    {
        //用户点击播放时再去实例化player对象
        player = new MpvPlayer(ui->screen);
        //进度条同步播放时间
        connect(player,&MpvPlayer::timePosChanged,this,&PlayerPage::onTimePosChanged);
        //绑定所有视频分片播放完毕发出的信号
        connect(player,&MpvPlayer::endOfPlaylist,this,&PlayerPage::startPlayer);
        //设置默认音量为50%
        player->setVolume(50.0);
    }
    loadBulletScreenData();//加载完毕弹幕数据后再进行播放
}

void PlayerPage::loadBulletScreenData()
{
    //向服务器发射弹幕数据获取请求
    model::DataCenter* dataCenter = model::DataCenter::getInstance();
    dataCenter->downloadVideoBarrage(videoInfo.videoId);
}

void PlayerPage::downloadVideoBarrageSuccess(const QString& videoId,const QJsonObject& barrages)
{
    QJsonArray barrageLists = barrages["barrageList"].toArray();
    //不是我当前视频的弹幕直接拒绝,弹幕数据为空也返回
    if(videoId != videoInfo.videoId || barrageLists.size() == 0)
    {
        return;
    }
    //提取弹幕数据
    QList<BulletScreenInfo> curbarrages;
    for(int i = 0;i < barrageLists.size();i++)
    {
        QJsonObject object = barrageLists[i].toObject();
        int64_t barrageTime = object["barrageTime"].toInteger();
        if(i != 0){
            //如果和上条弹幕时间相同,则直接插入
            int64_t lastBarrageTime = curbarrages.back().playTime;
            if(barrageTime != lastBarrageTime)
            {
                //时间不同则将目前的barrage插入哈希表中,清空再进行下次插入
                bulletScreenLists.insert(lastBarrageTime,curbarrages);
                curbarrages.clear();
            }
        }
        curbarrages.append(BulletScreenInfo(object["userId"].toString(),barrageTime,object["barrageContent"].toString()));
    }
    //将最后一段弹幕数据插入弹幕哈希集合中
    bulletScreenLists.insert(curbarrages.back().playTime,curbarrages);
    //弹幕数据加载完毕,开始视频数据的加载
    auto datacenter = model::DataCenter::getInstance();
    QString videoPath = datacenter->getHttpUrl() + "/HttpService/downloadVideo?fileId=" + videoInfo.videoFileId;
    player->startPlay(videoPath);
}

ok,播放视频与弹幕加载部分到这里也就完成了。对了,这里我们需要改一下原来playerpage时的关闭逻辑,具体逻辑是关闭时直接释放播放器对象以及mpv对象,当用户重新播放视频时再去new一个playerpage与mpv播放器对象。这样子改能显著降低我们播放器的内存消耗(同时存在一堆播放器内存占用肯定大啊,之前设计时没考虑到这一点)。大家根据自己的代码按照上面的逻辑进行修改即可。

三.播放界面数据更新

因为播放界面的上层videoBox已经存储有视频的相关信息了,所以我们直接在播放器的类中新增一个VideoInfo类型的成员变量并让Videobox在初始构造时传入视频信息即可。

//playerpage.cpp
void PlayerPage::updatePlayerPageUi()
{
    ui->videoTittle->setText(videoInfo.videoTitle);//标题
    ui->userNikeName->setText(videoInfo.nickname);//用户昵称
    ui->loadupTime->setText(videoInfo.videoUpTime);//更新时间
    ui->playNum->setText(intToString(videoInfo.playCount));//播放数
    ui->likeNum->setText(intToString(videoInfo.likeCount));//喜欢数
    ui->videoDesc->setText(videoInfo.videoDesc);//视频简介信息
    videoTotalTime = videoInfo.videoDuration;
    ui->videoDuration->setText(secondsFormat(0) + "/" + secondsFormat(videoTotalTime));//当前播放进度/播放总时长
}

void PlayerPage::setUserAvatar(const QPixmap &userAvatar)
{
    ui->userAvatar->setIcon(QIcon(userAvatar));
}

当然用户头像需要在videoBox界面获取到用户头像之后再进行获取。

//videobox.cpp
bool VideoBox::eventFilter(QObject *watched, QEvent *event)
{
    if(watched == ui->imageBox || watched == ui->videoTitleBox)
    {
        //如果是鼠标键按下才播放视频
        if(event->type() == QEvent::MouseButtonPress)
        {
            //初始化视频播放窗口-默认不显示-当点击视频界面时再去创建PlayerPage且不需要手动释放,窗口关闭时PlayerPage会自己释放
            videoPlayer = new PlayerPage(videoInfo);
            auto dataCenter = model::DataCenter::getInstance();
            videoPlayer->hide();
            videoPlayer->setUserAvatar(userImage);
            LOG() << "播放视频-打开播放窗口";
            videoPlayer->show();
            videoPlayer->startPlayer();
            //事件已处理
            return true;
        }
    }
    //其他事件继续向下传递
    return QObject::eventFilter(watched,event);
}

四.更新播放数

播放次数的更新通常与实际播放⾏为相关,例如当⽤⼾点击播放按钮并开始播放视频时,播放次数就会增加;如果⽤⼾只是打开视频但没有点击播放按钮,则播放次数通常不会更新,这是因为播放次数通常指⽤⼾实际观看了内容,⽽不是打开了⽂件。因此在PlayerPage的播放按钮槽函数中,可以增加⽂件的播放次数。
请求Url:

POST /HttpService/setPlay

请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
videoId string 视频ID

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息

那我们播放次数的增加就按照用户开始播放当前视频几次来计算吧,也就是说用户每触发一次startplayer我们给当前视频增加一次播放量(当然也可以视频结束时去增加播放量,无非就是把发送播放量改变的请求放到mpvplayer里面而已)。

//playerpage.cpp
void PlayerPage::downloadVideoBarrageSuccess(const QString& videoId,const QJsonObject& barrages)
{
    QJsonArray barrageLists = barrages["barrageList"].toArray();
    //不是我当前视频的弹幕直接拒绝,弹幕数据为空也返回
    if(videoId != videoInfo.videoId || barrageLists.size() == 0)
    {
        return;
    }
    //提取弹幕数据
    QList<BulletScreenInfo> curbarrages;
    for(int i = 0;i < barrageLists.size();i++)
    {
        QJsonObject object = barrageLists[i].toObject();
        int64_t barrageTime = object["barrageTime"].toInteger();
        if(i != 0){
            //如果和上条弹幕时间相同,则直接插入
            int64_t lastBarrageTime = curbarrages.back().playTime;
            if(barrageTime != lastBarrageTime)
            {
                //时间不同则将目前的barrage插入哈希表中,清空再进行下次插入
                bulletScreenLists.insert(lastBarrageTime,curbarrages);
                curbarrages.clear();
            }
        }
        curbarrages.append(BulletScreenInfo(object["userId"].toString(),barrageTime,object["barrageContent"].toString()));
    }
    //将最后一段弹幕数据插入弹幕哈希集合中
    bulletScreenLists.insert(curbarrages.back().playTime,curbarrages);
    //弹幕数据加载完毕,开始视频数据的加载
    auto datacenter = model::DataCenter::getInstance();
    QString videoPath = datacenter->getHttpUrl() + "/HttpService/downloadVideo?fileId=" + videoInfo.videoFileId;
    player->startPlay(videoPath);
    //更新视频播放数
    videoInfo.playCount++;
    datacenter->setPlayNumberAsync(videoInfo.videoId);
    datacenter->updateVideoPlayNumber(videoInfo.videoId,videoInfo.playCount);//发射更新请求并更新本地存储的数据
    ui->playNum->setText(intToString(videoInfo.playCount));//更新播放器界面的播放数
    emit this->videoBoxUpdatePlayNum(videoInfo.playCount);//更新videoBox界面的播放数
}

//videobox.cpp
bool VideoBox::eventFilter(QObject *watched, QEvent *event)
{
    if(watched == ui->imageBox || watched == ui->videoTitleBox)
    {
        //如果是鼠标键按下才播放视频
        if(event->type() == QEvent::MouseButtonPress)
        {
        	//......
            //绑定播放数更新的函数
            connect(videoPlayer,&PlayerPage::videoBoxUpdatePlayNum,this,[&](int64_t playNum){
                this->videoInfo.playCount = playNum;
                ui->playNum->setText(intToString(playNum));
            });
            //.....
        }
    }
    //其他事件继续向下传递
    return QObject::eventFilter(watched,event);
}

//datacenter.h
    void updateVideoPlayNumber(const QString& videoId,int64_t playcount);//更新视频的播放数
        //客户端发送更新播放数的请求
    void setPlayNumberAsync(const QString& videoId);
    signals:
    
//datacenter.cpp
void DataCenter::updateVideoPlayNumber(const QString &videoId, int64_t playcount)
{
    for(auto& videoInfo : homeVideoList->videoInfos)
    {
        if(videoInfo.videoId == videoId)
        {
            videoInfo.playCount = playcount;
            break;
        }
    }
}

void DataCenter::setPlayNumberAsync(const QString &videoId)
{
    netClient.setPlayNumber(videoId);
}

//netclient.cpp
void NetClient::setPlayNumber(const QString &videoId)
{
    /*
    {
        "requestId": "string",
        "sessionId": "string",
        "videoId": "string"
    }
    */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["videoId"] = videoId;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/setPlay",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        LOG() << "setPlay请求结束,修改播放次数成功, requestId: " << respBody["requestId"].toString();
    });
}

假服务端逻辑如下:

//mockserver.cpp
    //处理客户端播放数更新的请求
    httpServer.route("/HttpService/setPlay", [=](const QHttpServerRequest& req) {
        return this->setPlayNumber(req);
    });
QHttpServerResponse MockServer::setPlayNumber(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[setPlayNumber] 收到 setPlayNumber 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "更新播放量的videoId:" << reqData["videoId"].toString();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

因为我们这里是假服务器,就不再进行详细的播放数更新操作了,到服务端部分再。

五.检测视频是否点赞过

打开播放页面后,点赞按钮上图⽚应该根据用户是否对该视频点赞过合理显⽰,因此需要知道⽤⼾是否对该视频点赞过。
请求url:

POST /HttpService/judgeLike 

请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
videoId string 视频ID

返回响应:200 OK
响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
result object 结果信息
isLike boolean 是否点赞结果
//playerpage.cpp
void PlayerPage::downloadVideoBarrageSuccess(const QString& videoId,const QJsonObject& barrages)
{
	//.....
    //点赞相关
    datacenter->judgeLikeAsync(videoId);
}

//datacenter.h
    //判断当前用户是否对本视频点赞过
    void judgeLikeAsync(const QString& videoId);
        //判断视频是否被点赞成功响应
    void judgeLikeDone(const QString& videoId,bool isLike);
    
//datacenter.cpp
void DataCenter::judgeLikeAsync(const QString &videoId)
{
    netClient.judgeLike(videoId);
}

//netclient.cpp
void NetClient::setLike(const QString &videoId)
{
    /*
    {
        "requestId": "string",
        "sessionId": "string",
        "videoId": "string"
    }
    */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["videoId"] = videoId;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/setLike",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        LOG() << "setLike请求结束,获取到了当前用户对此视频的点赞情况";
    });
}

假服务端处理:

//mockserver.cpp
    //判断用户是否对相应视频点赞过
    httpServer.route("/HttpService/judgeLike", [=](const QHttpServerRequest& req) {
        return this->judgeLike(req);
    });

QHttpServerResponse MockServer::judgeLike(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[judgeLike] 收到 judgeLike 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    QJsonObject result;
    result["isLike"] = true;
    respData["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

老逻辑走完之后,我们需要根据服务端传回来的响应数据去改变点赞情况显示:

//videobox.cpp
bool VideoBox::eventFilter(QObject *watched, QEvent *event)
{
    if(watched == ui->imageBox || watched == ui->videoTitleBox)
    {
        //如果是鼠标键按下才播放视频
        if(event->type() == QEvent::MouseButtonPress)
        {
        	//....
            //检测用户是否对当前视频点赞过
            connect(dataCenter,&model::DataCenter::judgeLikeDone,videoPlayer,&PlayerPage::changeLikeStatu);
            //....
        }
    }
    //其他事件继续向下传递
    return QObject::eventFilter(watched,event);
}

//playerpage.cpp
void PlayerPage::changeLikeStatu(const QString &videoId, bool isLike)
{
    if(videoId != this->videoInfo.videoId){
        return;
    }
    likeCount = videoInfo.likeCount;//辅助判断用户在本次观看视频时其点赞情况是否更改
    this->isLike = isLike;
    if(isLike)
    {
        ui->likeImageBtn->setStyleSheet("border-image : url(:/images/PlayPage/dianzan.png)");
    }
}

六.更新点赞数

用户在观看视频时有可能会进行点赞操作表达对视频的喜爱。所以这里我们需要处理下,因为用户可能在观看视频是多次点击点赞按钮,所以我们最后播放窗口关闭时再去进行点赞情况的更新。
请求url:

POST /HttpService/setLike 

请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
videoId string 视频ID

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息

注意到我们上面判断用户是否对当前视频点赞过的添加的一个bool类型的成员变量likeCount了吗,最后如果该值不等于刚开始videobox传给我的videoinfo中的likecount时我们再更新:

//playerpage.cpp
void PlayerPage::onLikeImageBtnClicked()
{
    //todo
    if(false)
    {
        Login* login = new Login();
        Toast::showToast("登录之后才可以点赞哦~",login);
    }
    //因为用户在观看视频时可能频繁的点击点赞按钮,所以我们在关闭窗口时再去检验点赞情况发送如有需要修改请求即可
    isLike = !isLike;
    if(isLike){
        likeCount++;
        ui->likeImageBtn->setStyleSheet("border-image : url(:/images/PlayPage/dianzan.png)");
    }
    else{
        likeCount--;
        ui->likeImageBtn->setStyleSheet("border-image : url(:/images/PlayPage/quxiaodianzan.png)");
    }
    ui->likeNum->setText(intToString(likeCount));
}

void PlayerPage::playerClose()
{
    //判断点赞修改情况
    if(likeCount != videoInfo.likeCount)
    {
        videoInfo.likeCount = likeCount;
        //不相等时说明点赞情况已修改,需要向服务端发送请求
        auto dataCenter = model::DataCenter::getInstance();
        dataCenter->setLikeAsync(videoInfo.videoId);
        dataCenter->updateVideoLikeNumber(videoInfo.videoId,likeCount);
        emit this->videoBoxUpdateLikeNum(likeCount);
    }
    //关闭窗口时直接释放当前窗口对象,避免内存占用过大
    this->deleteLater();
}

//videobox.cpp
bool VideoBox::eventFilter(QObject *watched, QEvent *event)
{
    if(watched == ui->imageBox || watched == ui->videoTitleBox)
    {
        //如果是鼠标键按下才播放视频
        if(event->type() == QEvent::MouseButtonPress)
        {
        	//...
            connect(videoPlayer,&PlayerPage::videoBoxUpdateLikeNum,this,[&](int64_t likeNum){
                this->videoInfo.likeCount = likeNum;
                ui->likeNum->setText(intToString(likeNum));
            });
            //...
        }
    }
    //其他事件继续向下传递
    return QObject::eventFilter(watched,event);
}

//datacenter.h
    //当用户退出当前视频播放时,检验点赞情况有需要发送更新请求到服务端
    void setLikeAsync(const QString& videoId);
    void updateVideoLikeNumber(const QString& videoId,int64_t likeCount);//更新视频的点赞数
//datacenter.cpp
void DataCenter::setLikeAsync(const QString &videoId)
{
    netClient.setLike(videoId);
}

void DataCenter::updateVideoLikeNumber(const QString &videoId, int64_t likeCount)
{
    for(auto& videoInfo : homeVideoList->videoInfos)
    {
        if(videoInfo.videoId == videoId)
        {
            videoInfo.likeCount = likeCount;
            break;
        }
    }
}

//netclinet.cpp
void NetClient::setLike(const QString &videoId)
{
    /*
    {
        "requestId": "string",
        "sessionId": "string",
        "videoId": "string"
    }
    */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["videoId"] = videoId;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/setLike",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        LOG() << "setLike请求结束,获取到了当前用户对此视频的点赞情况";
    });
}

假服务端的处理逻辑:

//mockserver.cpp
    //客户端请求修改点赞情况
    httpServer.route("/HttpService/setLike", [=](const QHttpServerRequest& req) {
        return this->setLike(req);
    });

QHttpServerResponse MockServer::setLike(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[setLike] 收到 setLike 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "更新点赞情况的videoId:" << reqData["videoId"].toString();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

七.发送弹幕

⽤⼾在观看视频的过程中,如果⽤⼾有社交互动需求,或者视频内容引起⽤⼾的情感共鸣等,⽤⼾可能会发送弹幕,表达⾃⼰的情感或观点。发送弹幕前⽤⼾必须先登录,发送弹幕成功后,弹幕信息需同步到服务器。
请求url:

POST /HttpService/newBarrage

请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
videoId string 视频ID
barrageInfo object 弹幕信息
barrageContent string 弹幕内容
barrageTime integer 弹幕时间-相对秒数

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
//playerpage.cpp
void PlayerPage::onBulletLaunch(const QString &bullet)
{
    if(isShowBullet)
    {
    	//...
        //向服务端发射弹幕
        auto dataCenter = model::DataCenter::getInstance();
        dataCenter->newBarrageAsync(videoInfo.videoId,barrageInfo);
    }
}

//datacenter.cpp
void NetClient::newBarrage(const QString &videoId, const BulletScreenInfo &barrage)
{
    /*
    {
        "requestId": "string",
        "sessionId": "string",
        "videoId": "string",
        "barrageInfo": {
            "barrageContent":"string",
            "barrageTime": 0
        }
    }
    */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["videoId"] = videoId;
    QJsonObject barrageInfo;
    barrageInfo["barrageContent"] = barrage.text;
    barrageInfo["barrageTime"] = barrage.playTime;
    reqbody["barrageInfo"] = barrageInfo;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/newBarrage",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        LOG() << "newBarrage请求结束,客户端的弹幕发送成功";
    });
}

假服务端的处理逻辑:

//mockserver.cpp
    //客户端发射弹幕的请求
    httpServer.route("/HttpService/newBarrage", [=](const QHttpServerRequest& req) {
        return this->newBarrage(req);
    });

QHttpServerResponse MockServer::newBarrage(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[newBarrage] 收到 newBarrage 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "更新弹幕情况的videoId:" << reqData["videoId"].toString();
    QJsonObject barrageInfo = reqData["barrageInfo"].toObject();
    LOG() << "弹幕内容:" << barrageInfo["barrageContent"].toString() << ",视频对应时间:" << barrageInfo["barrageTime"].toInteger();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

八.我的界面部分-用户信息结构的设定

当⽤⼾点击主界⾯左侧栏我的⻚⾯切换按钮时,假设⽤⼾已经登录,此时该⻚⾯会加载当前登录⽤⼾的个⼈信息,并在界⾯上进⾏展⽰,包含⽤⼾头像、⽤⼾昵称、关注数、粉丝数、获赞数、播放数,以及⽤⼾之前上传的视频等信息,如下图所⽰:
在这里插入图片描述
注意:如果⽤⼾在播放⻚⾯观看视频时,想要查看视频上传者的信息,通过点击⽤⼾头像可以跳转到上传视频⽤⼾界⾯,此时就要获取视频上传者的个⼈信息,即获取其他⽤⼾信息。
在这里插入图片描述
为了⽅便对⽤⼾信息进⾏管理,定义如下描述⽤⼾信息结构:

//data.h
////////////////////////////
/////////用户信息的相关结构
////////////////////////////
enum RoleType{
    SuperAdmin = 1, // 超级管理员
    Admin, // 普通管理员
    User, // 普通⽤⼾
    TempUser // 临时⽤⼾
};
enum IdentityType{
    CUser = 1, // C 端⽤⼾
    BUser // B 端⽤⼾
};
class UserInfo
{
    public:
    QString userId; // ⽤⼾ID
    QString phoneNum; // ⽤⼾⼿机号
    QString nickname; // ⽤⼾昵称
    QList<int> roleType; // ⻆⾊类型:
    QList<int> identityType; // ⾝份类型:C端⽤⼾ 或 B端⽤⼾
    int64_t likeCount; // 点赞量
    int64_t playCount; // 播放量
    int64_t followedCount; // 关注数
    int64_t followerCount; // 粉丝数量
    int userStatus; // ⽤⼾状态
    int isFollowing; // 是否关注
    QString userMemo; // ⽤⼾备注信息
    QString userCTime; // ⽤⼾创建时间
    QString avatarFileId; // ⽤⼾头像Id
    // 将JSON对象转换为UserInfo
    void loadUserInfo(const QJsonObject& jsonObj);
    // 检测⽤⼾是否为B端用户
    bool isBUser()const;
    // 检测用户是否为临时用户
    bool isTempUser()const;
    //构建临时用户对象
    void buildTempUser();
};

//data.cpp
void UserInfo::loadUserInfo(const QJsonObject &jsonObj)
{
    /*
    "userId": "string",
    "phoneNum": "string",
    "nickname": "string",
    "roleType": [3],
    "identityType": [1],
    "likeCount": 0,
    "playCount": 0,
    "followedCount": 0,
    "followerCount": 0,
    "userStatus": 0,
    "isFollowing": 0,
    "userMemo": "string",
    "userCTime": "string",
    "avatarFileId":"string"
     */
    //先清空之前填入的信息
    this->identityType.clear();
    this->roleType.clear();
    //再填入新的信息
    userId = jsonObj["userId"].toString(); // ⽤⼾ID
    phoneNum = jsonObj["phoneNum"].toString(); // ⽤⼾⼿机号
    nickname = jsonObj["nickname"].toString(); // ⽤⼾昵称
    QJsonArray roleTypeList = jsonObj["roleType"].toArray();
    for(int i = 0;i < roleTypeList.size();i++)
    {
        roleType.append(roleTypeList[i].toInt());
    }
    QJsonArray identityTypeList = jsonObj["identityType"].toArray();
    for(int i = 0;i < identityTypeList.size();i++)
    {
        identityType.append(identityTypeList[i].toInt());
    }// ⾝份类型:C端⽤⼾ 或 B端⽤⼾
    likeCount = jsonObj["likeCount"].toInteger(); // 点赞量
    playCount = jsonObj["playCount"].toInteger();; // 播放量
    followedCount = jsonObj["followedCount"].toInteger();; // 关注数
    followerCount = jsonObj["followerCount"].toInteger();; // 粉丝数量
    userStatus = jsonObj["userStatus"].toInt(); // ⽤⼾状态
    isFollowing = jsonObj["isFollowing"].toInt(); // 是否关注
    userMemo = jsonObj["userMemo"].toString(); // ⽤⼾备注信息
    userCTime = jsonObj["userCTime"].toString(); // ⽤⼾创建时间
    avatarFileId = jsonObj["avatarFileId"].toString(); // ⽤⼾头像Id
}

bool UserInfo::isBUser() const
{
    for(auto& identity : identityType)
    {
        if(identity == BUser)
        {
            return true;
        }
    }
    return false;
}

bool UserInfo::isTempUser() const
{
    for(auto& role : roleType)
    {
        if(role == TempUser)
        {
            return true;
        }
    }
    return false;
}

void UserInfo::buildTempUser()
{
    userId = "";
    phoneNum = "";
    nickname = "临时用户";
    roleType.append(TempUser);
    identityType.append(CUser);
    likeCount = 0;
    playCount = 0;
    followedCount = 0;
    followerCount = 0;
    userStatus = 0;
    isFollowing = 0;
    userMemo = "";
    userCTime = "";
    avatarFileId = "";
}

让dataCenter持有⼀份⽤⼾信息的指针:

//datacenter.h
    void setMyselfUserInfo(const QJsonObject& userInfo);
    UserInfo* getMyselfUserInfo();//个人信息相关set与get接口
    void setOtherUserInfo(const QJsonObject& userInfo);
    UserInfo* getOtherUserInfo();//其他用户信息相关的get与set接口
    private:
    UserInfo* myselfUserInfo = nullptr;//当前用户的个人信息
    UserInfo* otherUserInfo = nullptr;//其他用户的个人信息

//datacenter.cpp
void DataCenter::setMyselfUserInfo(const QJsonObject &userInfo)
{
    if(myselfUserInfo == nullptr)
    {
        myselfUserInfo = new UserInfo();
    }
    myselfUserInfo->loadUserInfo(userInfo);
}

UserInfo *DataCenter::getMyselfUserInfo()
{
    return myselfUserInfo;
}

void DataCenter::setOtherUserInfo(const QJsonObject &userInfo)
{
    if(otherUserInfo == nullptr)
    {
        otherUserInfo = new UserInfo();
    }
    otherUserInfo->loadUserInfo(userInfo);
}

UserInfo *DataCenter::getOtherUserInfo()
{
    return otherUserInfo;
}

九.获取当前用户信息

请求url:

POST /Httpvice/getUserInfo

请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
userId string 用户ID

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
result object 响应结果
userInfo object 用户信息
userId string 用户ID
phoneNum string 绑定手机号
nickname string 用户昵称
roleType array 角色类型
identityType array 身份类型
likeCount integer 点赞量
playCount integer 播放量
followedCount integer 关注量
followerCount integer 粉丝量
userStatus integer 用户状态
isFollowing integer 是否已关注
userMemo string 用户备注信息
userCTime string 用户创建时间
avatarFileId string 头像文件ID

如果是⽤⼾获取⾃⼰的个⼈信息, 则在 json 中不填写 userId 属性, 服务器通过会话 id 来获取当前⽤⼾信息. 获取其他⽤⼾信息时,需要传递该⽤⼾的userId。服务器可以通过userId是否为空确认获取⾃⼰的个⼈信息还是其他⽤⼾的个⼈信息。
客⼾端添加获取⾃⼰和他⼈个⼈信息请求接⼝:

//datacenter.h
    //客户端请求自己和其他用户信息时的请求接口
    void getMyselfUserInfoAsync();
    void getOtherUserInfoAsync(const QString& userId);
    signals:
    //获取当前登录用户信息成功
    void getMyselfUserInfoDone();
    //获取其他用户信息成功
    void getOtherUserInfoDone();

//datacenter.cpp
void DataCenter::getMyselfUserInfoAsync()
{
    netClient.getUserInfo("");
}

void DataCenter::getOtherUserInfoAsync(const QString &userId)
{
    netClient.getUserInfo(userId);
}

//netclinet.cpp
void NetClient::getUserInfo(const QString &userId)
{
    /*
        "requestId": "string",
        "sessionId": "string",
        "userId": "string",
    */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["userId"] = userId;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/getUserInfo",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        QJsonObject result = respBody["result"].toObject();
        if(userId.isEmpty())
        {
            dataCenter->setMyselfUserInfo(result["userInfo"].toObject());
            emit dataCenter->getMyselfUserInfoDone();
        }
        else
        {
            dataCenter->setOtherUserInfo(result["userInfo"].toObject());
            emit dataCenter->getOtherUserInfoDone();
        }
        LOG() << "getUserInfo请求结束,用户信息获取成功";
    });
}

假服务端的处理逻辑:

//mockserver.cpp
    //客户端获取用户信息的请求
    httpServer.route("/HttpService/getUserInfo", [=](const QHttpServerRequest& req) {
        return this->getUserInfo(req);
    });

QHttpServerResponse MockServer::getUserInfo(const QHttpServerRequest &request)
{
    /*
    "userId": "string",
    "phoneNum": "string",
    "nickname": "string",
    "roleType": [3],
    "identityType": [1],
    "likeCount": 0,
    "playCount": 0,
    "followedCount": 0,
    "followerCount": 0,
    "userStatus": 0,
    "isFollowing": 0,
    "userMemo": "string",
    "userCTime": "string",
    "avatarFileId":"string"
     */
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    QString userId = reqData["userId"].toString();
    LOG() << "[getUserInfo] 收到 getUserInfo 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "收到客户端的用户信息请求,userId为:" << userId;
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    QJsonObject result;
    QJsonObject userInfo;
    if(userId == "")
    {
        //获取用户个人信息-构造一批假数据
        userInfo["userId"] = "10001";
        userInfo["phoneNum"] = "19000000001";
        userInfo["nickname"] = "114514";
        QJsonArray roleType;
        roleType.append(3);//普通用户
        userInfo["roleType"] = roleType;
        QJsonArray identityType;
        identityType.append(1);//C端用户
        userInfo["identityType"] = identityType;
        userInfo["likeCount"] = 114;//获赞数
        userInfo["playCount"] = 114;//播放量
        userInfo["followedCount"] = 1919;//关注数
        userInfo["followerCount"] = 1919;//粉丝数
        userInfo["userStatus"] = 0;//用户状态
        userInfo["isFollowing"] = 0;//是否关注
        userInfo["userMemo"] = "";//管理界面的备注信息
        userInfo["userCTime"] = "2025-1-14";//用户的注册日期
        userInfo["avatarFileId"] = "1000";//用户的头像路径
    }
    else
    {
        //获取其他用户的个人信息
        userInfo["userId"] = userId;
        userInfo["phoneNum"] = "19000000002";
        userInfo["nickname"] = "1919180";
        QJsonArray roleType;
        roleType.append(1);//超级管理员
        userInfo["roleType"] = roleType;
        QJsonArray identityType;
        identityType.append(2);//B端用户
        userInfo["identityType"] = identityType;
        userInfo["likeCount"] = 1919180;//获赞数
        userInfo["playCount"] = 1919180;//播放量
        userInfo["followedCount"] = 114;//关注数
        userInfo["followerCount"] = 114;//粉丝数
        userInfo["userStatus"] = 0;//用户状态
        userInfo["isFollowing"] = 0;//是否关注
        userInfo["userMemo"] = "超级管理员";//管理界面的备注信息
        userInfo["userCTime"] = "2025-1-1";//用户的注册日期
        userInfo["avatarFileId"] = "2000";//用户的头像路径
    }
    result["userInfo"] = userInfo;
    respData["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

此时我们在客户端的界面处理相关逻辑即可:

//myselfwidget.h
    void getMyselfUserInfo();//获取当前登录用户的个人信息
    void loadMyself();//当点击我的页面时,显示当前登录用户的个人信息
    //获取用户信息成功收到响应
    void getMyselfUserInfoDone();
private:
    // 绑定信号槽
    void connectSignalAndSlots();
	QString userId;//判断当前展示的是当前登录用户信息还是其他用户信息
//myselfwidget.cpp
void MyselfWidget::getMyselfUserInfo()
{
    auto dataCenter = model::DataCenter::getInstance();
    if(dataCenter->getMyselfUserInfo() == nullptr || dataCenter->getMyselfUserInfo()->isTempUser())
    {
        //此时用户信息还未获取,先获取
        dataCenter->getMyselfUserInfoAsync();
    }
    else
    {
        //此时用户信息已经获取,直接更新界面
        getMyselfUserInfoDone();
    }
}

void MyselfWidget::loadMyself()
{
    getMyselfUserInfo();
    //加载用户上传视频的信息
    //todo
    ui->avatarBtn->isHideMask(true);
    ui->avatarBtn->setEnabled(true);
}

void MyselfWidget::getMyselfUserInfoDone()
{
    auto dataCenter = model::DataCenter::getInstance();
    auto myselfInfo = dataCenter->getMyselfUserInfo();
    auto limePlayer = LimePlayer::getInstance();
    limePlayer->showSystemPageBtn(false);
    if(myselfInfo->isTempUser())
    {
        //当前登录用户为临时用户,需要隐藏登录用户相关的控件
        ui->avatarBtn->setIcon(QIcon(":/image/myself/defaultAvatar.png"));
        ui->nicknameBtn->setText("点击登录");
        ui->nicknameBtn->adjustSize();//根据文本调整控件大小
        ui->avatarBtn->setEnabled(false);
        ui->nicknameBtn->setEnabled(true);
        hideWidget(true);
        return;
    }
    else if(myselfInfo->isBUser())
    {
        //说明当前登录用户为B端用户,需要显示管理界面
        limePlayer->showSystemPageBtn(true);
    }
    //普通用户及以上权限用户的共同处理部分
    hideWidget(false);
    ui->nicknameBtn->setText(myselfInfo->nickname);
    ui->nicknameBtn->adjustSize();
    QRect rect = ui->nicknameBtn->geometry();//调整设置按钮的位置
    ui->settingBtn->move(rect.x() + rect.width() + 8, ui->settingBtn->geometry().y());
    ui->attentionCountLabel->setText(intToString2(myselfInfo->followedCount));
    ui->playCountLabel->setText(intToString2(myselfInfo->playCount));
    ui->likeCountLabel->setText(intToString2(myselfInfo->likeCount));
    ui->fansCountLabel->setText(intToString2(myselfInfo->followerCount));
    ui->attentionBtn->hide();
    ui->myVideolabel->setText("我的视频");
    ui->nicknameBtn->setEnabled(false);
    if(myselfInfo->avatarFileId.isEmpty())
    {
        //如果用户之前没有设置过头像,那么使用默认头像
        ui->avatarBtn->setIcon(QIcon(":/images/myself/defaultAvatar.png"));
    }
    else
    {
        //使用用户设置的头像
        dataCenter->downloadPhotoAsync(myselfInfo->avatarFileId);
    }
    userId = "";
}

void MyselfWidget::hideWidget(bool isHide)
{
    // 临时⽤⼾需要隐藏界⾯上控件,⾮临时⽤⼾显⽰控件
    if(isHide){
        ui->attentionBtn->hide();
        ui->attentionCountLabel->hide();
        ui->attentionLabel->hide();
        ui->fansLabel->hide();
        ui->fansCountLabel->hide();
        ui->likeLabel->hide();
        ui->likeCountLabel->hide();
        ui->playLabel->hide();
        ui->playCountLabel->hide();
        ui->settingBtn->hide();
        ui->quitBtn->hide();
        ui->uploadVideoBtn->hide();
        // scrollArea隐藏后,控件的位置仍旧保留
        QSizePolicy sizePolicy = ui->scrollArea->sizePolicy();
        sizePolicy.setRetainSizeWhenHidden(true);
        ui->scrollArea->setSizePolicy(sizePolicy);
        ui->scrollArea->hide();
        sizePolicy = ui->titleBar->sizePolicy();
        sizePolicy.setRetainSizeWhenHidden(true);
        ui->titleBar->setSizePolicy(sizePolicy);
        ui->titleBar->hide();
    }else{
        ui->attentionBtn->show();
        ui->attentionCountLabel->show();
        ui->attentionLabel->show();
        ui->fansLabel->show();
        ui->fansCountLabel->show();
        ui->likeLabel->show();
        ui->likeCountLabel->show();
        ui->playLabel->show();
        ui->playCountLabel->show();
        ui->settingBtn->show();
        ui->quitBtn->show();
        ui->uploadVideoBtn->show();
        ui->scrollArea->show();
        ui->titleBar->show();
    }
}

void MyselfWidget::connectSignalAndSlots()
{
    connect(ui->settingBtn,&QPushButton::clicked,this,&MyselfWidget::onModifyMyselfClicked);
    connect(ui->uploadVideoBtn,&QPushButton::clicked,this,&MyselfWidget::onUpLoadVideoBtnClicked);
    auto dataCenter = model::DataCenter::getInstance();
    connect(dataCenter,&model::DataCenter::getMyselfUserInfoDone,this,&MyselfWidget::getMyselfUserInfoDone);
    connect(dataCenter,&model::DataCenter::downloadPhotoDone,this,&MyselfWidget::setUserAvatar);//当用户头像下载成功之后设置用户头像
}

void MyselfWidget::setUserAvatar(const QString &fileId, const QByteArray &photoData)
{
    auto myselfInfo = model::DataCenter::getInstance()->getMyselfUserInfo();
    if(myselfInfo != nullptr && fileId == myselfInfo->avatarFileId)
    {
        ui->avatarBtn->setIcon(makeCircleIcon(photoData,ui->avatarBtn->height()/2));
    }
}

//如果非b端用户,则需要通知主界面隐藏系统界面按钮:
//limeplayer.cpp
void LimePlayer::showSystemPageBtn(bool isShow)
{
    if(isShow)
    {
        ui->sysPageBtn->show();
    }
    else
    {
        QSizePolicy sizePolicy = ui->sysPageBtn->sizePolicy();
        sizePolicy.setRetainSizeWhenHidden(true);
        ui->sysPageBtn->setSizePolicy(sizePolicy);
        ui->sysPageBtn->hide();
    }
}

void LimePlayer::switchPage(ButtonType buttonType)
{
    else if(buttonType == MyPageBtn)
    {
    	//...
        ui->myPage->loadMyself();
    }
}

十.上传与修改用户头像

⽤⼾登录之后,可以通过点击⽤⼾头像按钮来修改头像,修改图像需要:
• 从磁盘获取新图⽚作为⽤⼾头像
• 上传⽤⼾图像到服务器
• 图⽚上传成功后,⽤返回的图⽚id修改服务器中⽤⼾信息中头像id
• ⽤⼾头像修改成功后,⽤新头像id重新下载图⽚,并将图⽚设置到控件中

其中从磁盘获取⽤⼾图像的操作已经在uploadAvatarBtnClicked函数中完成完成
请求URL: POST /HttpService/uploadPhoto?requestId=xxx&sessionId=xxx
请求参数

Content-Type: application/octet-stream

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
body binary 文件数据

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
result object 响应结果
fileId string 文件ID

客⼾端新增上传图⽚请求:

//avatarbutton.cpp
void AvatarButton::onAvatarBtnClicked()
{
    //裁剪并设置用户上传的头像
    //this->setIcon(makeCircleIcon(byteArray,30));
    //上传图片数据到服务端
    //...
    dataCenter->uploadPhotoAsync(byteArray);
}

//datacenter.h
    //客户端请求修改自己头像的请求
    void uploadPhotoAsync(const QByteArray& photoData);
    signals:
    //向服务端上传图片请求成功响应
    void uploadPhotoDone(const QString& fileId);

//datacenter.cpp
void DataCenter::uploadPhotoAsync(const QByteArray &photoData)
{
    netClient.uploadPhoto(photoData);
}

//netclient.cpp
void NetClient::uploadPhoto(const QByteArray &photoData)
{
    // 1. 构造请求
    QString queryString;
    queryString += "requestId=";
    queryString += makeRequestId();
    queryString += "&";
    queryString += "sessionId=";
    queryString += dataCenter->getSessionId();
    // 2. 发送请求
    QNetworkRequest httpReq;
    httpReq.setUrl(QUrl(HTTP_URL + "/HttpService/uploadPhoto?" + queryString));
    httpReq.setHeader(QNetworkRequest::ContentTypeHeader, "application/octetstream");
    QNetworkReply* reply = netClientManager.post(httpReq,photoData);
    // 3. 异步处理响应
    connect(reply, &QNetworkReply::finished, this, [=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        QJsonObject result = respBody["result"].toObject();
        emit dataCenter->uploadPhotoDone(result["fileId"].toString());
        LOG() << "uploadPhoto请求结束,图片上传成功,fileId为:" << result["fileId"].toString();
    });
}

假服务端处理逻辑:

//mockserver.cpp
    //客户端上传图片的请求
    httpServer.route("/HttpService/uploadPhoto",[=](const QHttpServerRequest& req){
        return this->uploadPhoto(req);
    });

QHttpServerResponse MockServer::uploadPhoto(const QHttpServerRequest &request)
{
    // 解析查询字符串
    QUrlQuery query(request.url());
    QString requestId = query.queryItemValue("requestId");
    QString sessionId = query.queryItemValue("sessionId");
    LOG() << "[uploadPhoto] 收到 uploadPhoto 请求, requestId=" << requestId << "sessionId =" << sessionId;
    idPathTable[5000] = "/images/temp.png";
    // 构造图⽚路径
    QDir dir(QDir::currentPath());
    dir.cdUp();
    dir.cdUp();
    writeByteArrayToFile(dir.absolutePath() + "/images/temp.png",request.body());
    // 构造响应json报文
    QJsonObject respBody;
    respBody["requestId"] = requestId;
    respBody["errorCode"] = 0;
    respBody["errorMsg"] = "";
    QJsonObject result;
    result["fileId"] = "5000";
    respBody["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respBody).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

客户端收到新头像文件的id时,再根据此id向服务端获取新的头像:
请求URL: POST /HttpService/setAvatar
请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
fileId string 文件ID

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
//myselfwidget.h
    //图⽚上传分两个阶段处理:①上传图⽚⽂件 ②修改图⽚
    void uploadPhotoDone1(const QString& fileId);
    void uploadPhotoDone2();

//myselfwidget.cpp
void MyselfWidget::connectSignalAndSlots()
{
    connect(ui->settingBtn,&QPushButton::clicked,this,&MyselfWidget::onModifyMyselfClicked);
    connect(ui->uploadVideoBtn,&QPushButton::clicked,this,&MyselfWidget::onUpLoadVideoBtnClicked);
    auto dataCenter = model::DataCenter::getInstance();
    connect(dataCenter,&model::DataCenter::getMyselfUserInfoDone,this,&MyselfWidget::getMyselfUserInfoDone);
    connect(dataCenter,&model::DataCenter::downloadPhotoDone,this,&MyselfWidget::setUserAvatar);//当用户头像下载成功之后设置用户头像
    connect(dataCenter,&model::DataCenter::uploadPhotoDone,this,&MyselfWidget::uploadPhotoDone1);//服务端获取到用户上传的图片数据之后,返回新的头像id
    connect(dataCenter,&model::DataCenter::setAvatarDone,this,&MyselfWidget::uploadPhotoDone2);
}

void MyselfWidget::uploadPhotoDone1(const QString &fileId)
{
    //首先更新当前用户的头像Id
    auto dataCenter = model::DataCenter::getInstance();
    dataCenter->setAvatarId(fileId);
    //发送设置用户头像请求
    dataCenter->setAvatarAsync(fileId);
}

void MyselfWidget::uploadPhotoDone2()
{
    //服务端更新头像文件ID完毕,重新申请设置当前客户端展示的用户头像
    auto dataCenter = model::DataCenter::getInstance();
    auto myselfInfo = dataCenter->getMyselfUserInfo();
    dataCenter->downloadPhotoAsync(myselfInfo->avatarFileId);
}

//datacenter.cpp
void DataCenter::setAvatarId(const QString &fileId)
{
    //更新当前用户头像对应的文件Id
    this->myselfUserInfo->avatarFileId = fileId;
}

void DataCenter::setAvatarAsync(const QString &fileId)
{
    netClient.setAvatar(fileId);
}

//netclient.cpp
void NetClient::setAvatar(const QString &fileId)
{
    /*
        "requestId": "string",
        "sessionId": "string",
        "setAvatar": "string",
    */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["fileId"] = fileId;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/setAvatar",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        emit dataCenter->setAvatarDone();
        LOG() << "setAvatar请求结束,用户信息获取成功";
    });
}

假服务端处理逻辑:

//mockserver.cpp
    //客户端请求更新当前用户头像文件Id的请求
    httpServer.route("/HttpService/setAvatar",[=](const QHttpServerRequest& req){
        return this->setAvatar(req);
    });
    QHttpServerResponse MockServer::setAvatar(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[setAvatar] 收到 setAvatar 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "更新用户头像文件Id的fileId:" << reqData["fileId"].toString();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

十一.获取用户视频列表

其实逻辑与之前获取首页视频列表差不多,不过因为考虑到管理界面的原因,我们需要新增几个字段:

//data.h
////////////////////////////
/////////单条视频的信息结构
////////////////////////////
// 视频状态
enum VideoStatus{
    noStatus = 0, // ⽆状态
    waitForChecking, // 待审核
    putaway, // 审核通过 or 上架
    reject, // 审核驳回
    discard // 已下架
};
class VideoInfo{
public:
	//...
    int videoStatus; // 视频状态
    QString checkerId; // 审核者id
    QString checkerName; // 审核者昵称
    QString checkerAvatar; // 审核者⽤⼾头像id

    //加载json对象到单个VideoInfo中
    void loadJsonResultToVideoInfo(const QJsonObject& result);
};
//data.cpp
////////////////////////////
/////////单条视频的信息结构
////////////////////////////
void VideoInfo::loadJsonResultToVideoInfo(const QJsonObject &result)
{
    videoId = result["videoId"].toString();
    userId = result["userId"].toString();
    userAvatarId = result["userAvatarId"].toString();
    nickname = result["nickname"].toString();
    videoFileId = result["videoFileId"].toString();
    photoFileId = result["photoFileId"].toString();
    likeCount = result["likeCount"].toInteger();
    playCount = result["playCount"].toInteger();
    videoSize = result["videoSize"].toInteger();
    videoDesc = result["videoDesc"].toString();
    videoTitle = result["videoTitle"].toString();
    videoDuration = result["videoDuration"].toInteger();
    videoUpTime = result["videoUpTime"].toString();
    //以下为给管理员看的信息
    videoStatus = result["videoStatus"].toInt();
    checkerId = result["checkerId"].toString();
    checkerName = result["checkerName"].toString();
    checkerAvatar = result["checkerAvatar"].toString();
}

前面的代码不需要修改,因为首页视频列表也不需要新增的这几个字段。
那么接下来我们的思路就是,在datacenter中新增一个用户视频列表。跟获取用户信息部分一样,传入的userId为空则标识获取当前登录用户的视频列表,不为空则是获取其他用户视频列表。
请求url:POST /HttpService/userVideoList
请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
userId string 用户ID
pageIndex integer 页码
pageCount integer 每页条目数量

返回响应:200 OK 按时间排序

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
result object 响应结果
totalCount integer 总数量
videoList array 视频列表
videoId string 视频ID
userId string 所属用户ID
userAvatarId string 用户头像ID
nickname string 所属用户名称
checkerId string 审核者用户ID
checkerAvatar string 审核者用户头像ID
checkerName string 审核者用户名称
videoFileId string 视频文件ID
photoFileId string 封面文件ID
likeCount integer 点赞数量
playCount integer 播放数量
videoSize integer 视频大小
videoDesc string 视频简介
videoTitle string 视频标题
videoDuration integer 视频时长
videoUpTime string 视频上架时间
videoStatus integer 视频状态
//datacenter.h
    //用户界面的视频列表
    VideoList* userVideoList = nullptr;
    //客户端请求用户视频列表的请求-userId为空请求当前用户,不为空请求其他用户
    void getUserVideoListAsync(const QString& userId,int pageIndex);

//datacenter.cpp
void DataCenter::getUserVideoListAsync(const QString &userId, int pageIndex)
{
    netClient.getUserVideoList(userId,pageIndex);
}

//netclient.cpp
void NetClient::getUserVideoList(const QString &userId, int pageIndex)
{
    /*
     * {
        "requestId": "string",
        "sessionId": "string",
        "userId": "string",
        "pageIndex": 0,
        "pageCount": 0
       }
     */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["userId"] = userId;
    reqbody["pageIndex"] = pageIndex;
    reqbody["pageCount"] = model::VideoList::PAGE_COUNT;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/userVideoList",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        QJsonObject result = respBody["result"].toObject();
        dataCenter->setUserVideoList(result);
        LOG() << "getUserVideoList请求结束,获取用户视频列表请求成功!";
        emit dataCenter->getUserVideoListDone(userId);
    });
}

假服务端的处理逻辑:

//mockserver.cpp
    //客户端用户视频列表的获取请求
    httpServer.route("/HttpService/userVideoList",[=](const QHttpServerRequest& req){
        return this->getUserVideoList(req);
    });
QHttpServerResponse MockServer::getUserVideoList(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    QString uploadUserId = reqData["userId"].toString();
    LOG() << "[getUserVideoList] 收到 getUserVideoList 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString() << "userId = " << uploadUserId;
    //获取页号与获取视频数目
    int pageCount = reqData["pageCount"].toInt();
    int64_t pageIndex = reqData["pageIndex"].toInteger();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    QJsonObject result;
    //总视频个数设置为100
    result["totalCount"] = 100;
    if(uploadUserId.isEmpty())
    {
        QJsonArray videoList;
        //构造假数据
        int videoId = 10000;
        int userId = 10000;
        int resourceId = 1000;
        int maxVideoNum = qMin(100,pageCount * pageIndex);
        for(int i = pageCount * (pageIndex - 1); i < maxVideoNum; ++i){
            QJsonObject videoJsonObj;
            videoJsonObj["videoId"] = QString::number(videoId++);
            videoJsonObj["userId"] = QString::number(userId++);
            videoJsonObj["nickname"] = "咻114514";
            videoJsonObj["userAvatarId"] = QString::number(resourceId++);
            videoJsonObj["photoFileId"] = QString::number(resourceId++);
            videoJsonObj["videoFileId"] = QString::number(resourceId++);
            videoJsonObj["likeCount"] = 9867;
            videoJsonObj["playCount"] = 2105;
            videoJsonObj["videoSize"] = 10240;
            //videoJsonObj["videoDesc"] = "『Bad Apple!! feat.SEKAI』\n25時、ナイトコードで。 × 初音ミク\n作詞:Haruka-作曲:ZUN(上海アリス幻樂団)-編曲:ビートまりお×まろん、まらしぃ、Masayoshi Minoshima";
            videoJsonObj["videoDesc"] = "111";
            //videoJsonObj["videoTitle"] = "【25時、ナイトコードで。 × 初音ミク】Bad Apple!!【2DMV/世界计划 × 东方project 联动收录曲】";
            videoJsonObj["videoTitle"] = "111";
            videoJsonObj["videoDuration"] = 190;
            videoJsonObj["videoUpTime"] = "2024-05-04 17:11:11";
            //假审核人员数据
            videoJsonObj["videoStatus"] = 0;
            videoJsonObj["checkerId"] = "114514";
            videoJsonObj["checkerName"] = "咻114514";
            videoJsonObj["checkerAvatar"] = "5000";
            videoList.append(videoJsonObj);
        }
        result["videoList"] = videoList;
    }
    else{
        QJsonArray videoList;
        //构造假数据
        int videoId = 20000;
        int userId = 20000;
        int resourceId = 2000;
        int maxVideoNum = qMin(100,pageCount * pageIndex);
        for(int i = pageCount * (pageIndex - 1); i < maxVideoNum; ++i){
            QJsonObject videoJsonObj;
            videoJsonObj["videoId"] = QString::number(videoId++);
            videoJsonObj["userId"] = QString::number(userId++);
            videoJsonObj["nickname"] = "咻";
            videoJsonObj["userAvatarId"] = QString::number(resourceId++);
            videoJsonObj["photoFileId"] = QString::number(resourceId++);
            videoJsonObj["videoFileId"] = QString::number(resourceId++);
            videoJsonObj["likeCount"] = 9867;
            videoJsonObj["playCount"] = 2105000;
            videoJsonObj["videoSize"] = 10240;
            videoJsonObj["videoDesc"] = "中文翻译取自巴哈姆特 - 月勳 / 星櫻@翻譯委託開放 / 榎宮月 / 三無氣體 | b站 - 萌萌哒汪帕 \n音频取自:砂塚タカシ\n宵崎奏 (CV:楠木灯)";
            videoJsonObj["videoTitle"] = "25時、ナイトコードで。ANVO专 - 宵崎奏 | 中字 - Cutlery";
            videoJsonObj["videoDuration"] = 237;
            videoJsonObj["videoUpTime"] = "2024-05-10 21:46:59";
            //假审核人员数据
            videoJsonObj["videoStatus"] = 0;
            videoJsonObj["checkerId"] = "114514";
            videoJsonObj["checkerName"] = "咻114514";
            videoJsonObj["checkerAvatar"] = "5000";
            videoList.append(videoJsonObj);
        }
        result["videoList"] = videoList;
    }
    respData["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

客户端对收到的用户视频列表进行展示:

//myselfwidget.cpp
void MyselfWidget::getUserVideoListDone(const QString &userId)
{
    auto* dataCenter = model::DataCenter::getInstance();
    auto userVideoList = dataCenter->getUserVideoListPtr();
    //每一行显示4个视频
    const int rowCount = 4;
    for (int i = ui->layout->count(); i < userVideoList->getVideoCount(); i++)
    {
        int row = i / rowCount;
        int col = i % rowCount;
        VideoBox* videoBox = new VideoBox(userVideoList->videoInfos[i]);
        ui->layout->addWidget(videoBox, row, col);
    }
    //对视频列表的pageIndex进行++,方便下一次获取视频列表
    userVideoList->pageIndex++;
}

//发射获取用户视频列表请求:
void MyselfWidget::getUserVideoList(const QString &userId, int pageIndex)
{
    auto* dataCenter = model::DataCenter::getInstance();
    auto userVideoList = dataCenter->getUserVideoListPtr();
    if(pageIndex == 1)
    {
        //说明此时有可能是切换不同用户界面显示了,所以要清空界面中展示的视频
        clearVideoList();
    }
    dataCenter->getUserVideoListAsync(userId, pageIndex);
}

void MyselfWidget::loadMyself()
{
    getMyselfUserInfo();
    //加载用户上传视频的信息
    getUserVideoList("",1);
    ui->avatarBtn->isHideMask(true);
    ui->avatarBtn->setEnabled(true);
}

//获取下一页视频列表:
void MyselfWidget::clearVideoList()
{
    auto dataCenter = model::DataCenter::getInstance();
    auto userVideoList = dataCenter->getUserVideoListPtr();
    //清空用户视频列表中的数据
    userVideoList->clearVideoList();
    //清空我的界面中的视频数据
    QLayoutItem* VideoItem;
    while ((VideoItem = ui->layout->takeAt(0)) != nullptr) {
        // 先删除item中的widget(如果有的话)
        if (QWidget* widget = VideoItem->widget()) {
            delete widget;
        }
        // 然后删除item本身
        delete VideoItem;
    }
}

void MyselfWidget::onSrcollAreaValueChanged(int value)
{
    if(value == 0)
    {
        return;
    }
    // 正在更新视频,再出发该信号时选择忽略,将以此更新的视频信息显⽰到界⾯后再处理下⼀次更新
    if(value == ui->scrollArea->verticalScrollBar()->maximum()){
        //继续获取下一页视频的数据
        auto dataCenter = model::DataCenter::getInstance();
        auto userVideListPtr = dataCenter->getUserVideoListPtr();
        dataCenter->getUserVideoListAsync(userId, userVideListPtr->getPageIndex());
    }
}

十二.删除视频

我的⻚⾯中,当前登录⽤⼾可以通过点击视频框中的 … 按钮,删除之前上传的视频。
注意,… 按钮只有在我的⻚⾯中使⽤的VideoBox中才显⽰,⾸⻚和其他⽤⼾⻚⾯中的VideoBox中…都是隐藏的,即⾸⻚和其他⻚⾯中的视频是不能删除的。
在VideoBox中,设置删除按钮的隐藏和显式⽅式,并给该按钮绑定槽函数,当按钮点击时发射信号通过我的⻚⾯,删除列表中的视频。

//videobox.cpp
void VideoBox::showDelBtn(bool isshow)
{
    if(isshow)
    {
        ui->delVideoBtn->show();
    }
    else{
        ui->delVideoBtn->hide();
    }
}

void VideoBox::onDelBtnClicked()
{
    // 定义菜单的样式
    QString style = "QMenu { "
                    "background-color:#FFFFFF;"
                    "border:none;"
                    "border-radius: 6px;"
                    "padding: 0; }";
    style += "QMenu::item { "
             "background-color:#FFFFFF;"
             "border: none; "
             "border-radius: 6px;"
             "min-width: 50px;"
             "min-height: 32px;""font-size: 12px;"
             "color: #222222;"
             "padding-left: 24px;}";
    style += "QMenu::item:selected { "
             "background-color: rgb(62, 206, 254); "
             "color: #FFFFFF; }";
    QMenu menu(this);
    menu.setStyleSheet(style);
    // 要想让 QMenu 圆⻆⽣效, 需要设置下列两步操作1.去掉窗⼝框架和阴影2.设置为透明窗口
    menu.setWindowFlags(menu.windowFlags() | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
    menu.setAttribute(Qt::WA_TranslucentBackground, true);
    // 添加菜单项
    menu.addAction("删除");
    // 在鼠标点击位置弹出上下文菜单
    QPoint point = QCursor::pos();
    QAction* action = menu.exec(point);
    //action为用户选择的指定菜单项对应的QAction对象的指针;如果用户取消菜单则返回nullptr
    if (action == nullptr) {
        return;
    }
    if (action->text() == "删除") {
        LOG() << "删除视频: " << videoInfo.videoId;
        //向服务器发射删除视频的信号
        emit deleteVideo(videoInfo.videoId);
    }
}

//myselfwidget.cpp
void MyselfWidget::getUserVideoListDone(const QString &userId)
{
    for (int i = ui->layout->count(); i < userVideoList->getVideoCount(); i++)
    {
    	//...
        if(userId.isEmpty())
            connect(videoBox,&VideoBox::deleteVideo,this,&MyselfWidget::delMyVideo);
        if(userId.isEmpty())
        {
            videoBox->showDelBtn(true);
        }
        //...
    }
}

接下来便需要告诉服务端我要删除视频,需要注意的时删除视频时,传⼊的是视频ID,⽽不是视频⽂件ID,因为需要删除数据库的视频元信息。
请求url:POST /HttpService/removeVideo
请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
videoId string 视频ID

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
//myselfwidget.cpp
void MyselfWidget::delMyVideo(const QString &videoId)
{
    auto dataCenter = model::DataCenter::getInstance();
    dataCenter->deleteVideoAsync(videoId);
}

void MyselfWidget::onDeleteVideoDone()
{
    //这里必定是当前用户自己的视频被删除,所以不使用userId
    getUserVideoList("",1);
}

//datacenter.cpp
void DataCenter::deleteVideoAsync(const QString &videoId)
{
    netClient.deleteVideo(videoId);
}

//netclient.cpp
void NetClient::deleteVideo(const QString &videoId)
{
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["videoId"] = videoId;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/removeVideo",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        emit dataCenter->deleteVideoDone();
        LOG() << "removeVideo请求结束,服务端已将对应视频删除了";
    });
}

假服务端的处理逻辑:

//mockserver.cpp
    //客户端请求删除自己当前登录用户视频的请求
    httpServer.route("/HttpService/removeVideo",[=](const QHttpServerRequest& req){
        return this->deleteVideo(req);
    });
    QHttpServerResponse MockServer::deleteVideo(const QHttpServerRequest &request)
{
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[deleteVideo] 收到 deleteVideo 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "删除对应的视频,视频Id为:" << reqData["videoId"].toString();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

十三.获取其他用户信息

目前我们只有一个地方就是播放器界面的头像按钮,需要点击之后跳转到其他用户信息界面:

//playerpage.cpp
void PlayerPage::onAvatarBtnClicked()
{
    LimePlayer::getInstance()->showUserInfoOnPage(videoInfo.userId);
    playerClose();//关闭当前窗口
}

//limeplayer.cpp
void LimePlayer::showUserInfoOnPage(const QString &userId)
{
    //切换界面到当前对应的userId的用户个人信息界面
    ui->stackedWidget->setCurrentIndex(MyPageBtn);
    ui->myPage->loadOtherUser(userId);
}

接下来不需要新的请求,直接使用我们前面写好的请求即可获取其他用户信息:

//myselfwidget.cpp
void MyselfWidget::loadOtherUser(const QString &userId)
{
    getOtherUserInfo(userId);
    //加载当前用户上传视频的信息
    getUserVideoList(userId,1);
    ui->avatarBtn->isHideMask(false);
    ui->avatarBtn->setEnabled(false);
}

void MyselfWidget::getOtherUserInfo(const QString &userId)
{
    auto dataCenter = model::DataCenter::getInstance();
    auto otherUserInfo = dataCenter->getOtherUserInfo();
    if(otherUserInfo == nullptr || otherUserInfo->userId != userId)
    {
        //此时对应userId的用户信息还未获取,先获取
        dataCenter->getOtherUserInfoAsync(userId);
    }
    else
    {
        //此时用户信息已经获取过了,直接更新界面
        getOtherUserInfoDone();
    }
}

void MyselfWidget::getOtherUserInfoDone()
{
    auto dataCenter = model::DataCenter::getInstance();
    auto otherUserInfo = dataCenter->getOtherUserInfo();
    userId = otherUserInfo->userId;//更新用户Id
    //因为这里是其他用户,是能够从服务端获取其对应信息的,所以必定为非临时用户
    hideWidget(false);
    ui->nicknameBtn->setText(otherUserInfo->nickname);
    ui->nicknameBtn->adjustSize();
    QRect rect = ui->nicknameBtn->geometry();//调整设置按钮的位置
    ui->settingBtn->move(rect.x() + rect.width() + 8, ui->settingBtn->geometry().y());
    ui->attentionCountLabel->setText(intToString2(otherUserInfo->followedCount));
    ui->playCountLabel->setText(intToString2(otherUserInfo->playCount));
    ui->likeCountLabel->setText(intToString2(otherUserInfo->likeCount));
    ui->fansCountLabel->setText(intToString2(otherUserInfo->followerCount));
    ui->quitBtn->hide();
    ui->myVideolabel->setText("TA的视频");
    ui->nicknameBtn->setEnabled(false);
    ui->uploadVideoBtn->hide();
    if(otherUserInfo->avatarFileId.isEmpty())
    {
        //如果用户之前没有设置过头像,那么使用默认头像
        ui->avatarBtn->setIcon(QIcon(":/images/myself/defaultAvatar.png"));
    }
    else
    {
        //使用用户设置的头像
        dataCenter->downloadPhotoAsync(otherUserInfo->avatarFileId);
    }
}

void MyselfWidget::setUserAvatar(const QString &fileId, const QByteArray &photoData)
{
    auto otherUserInfo = model::DataCenter::getInstance()->getOtherUserInfo();
    if(otherUserInfo != nullptr && fileId == otherUserInfo->avatarFileId)
    {
        ui->avatarBtn->setIcon(makeCircleIcon(photoData,ui->avatarBtn->height()/2));
    }
}

十四.关注与取消关注

当发布视频⽤⼾的关注状态为被关注或者被取消关注时,关注按钮上⽂本和样式是不⼀样的,将来需要根据按钮的点击设置关注或取消关注,为简单起⻅,下⾯对关注按钮简单进⾏封装。

//myselfwidget.h
///////////////////////////////////////AttentionButton部分
class AttentionButton : public QPushButton
{
    Q_OBJECT
public:
    explicit AttentionButton(QWidget* parent = nullptr);
    bool attentionStatu();//当前处于关注还是未关注状态
    void changeAttentionStatus(bool attentionStatu);//当用户点击按钮之后改变按钮状态
private:
    bool isAttention;
};

//myselfwidget.cpp
///////////////////////////////////////AttentionButton部分
AttentionButton::AttentionButton(QWidget *parent)
    :QPushButton(parent),
    isAttention(false)
{
    changeAttentionStatus(false);
}

bool AttentionButton::attentionStatu()
{
    return isAttention;
}

void AttentionButton::changeAttentionStatus(bool attentionStatu)
{
    this->isAttention = attentionStatu;
    if(attentionStatu)
    {
        this->setText("已关注");
        this->setStyleSheet("QPushButton{"
                            "background-color: transparent;"
                            "color: #3ECEFE;"
                            "font-size : 14px;"
                            "border-radius: 18px;"
                            "border: 1px solid #3ECEFE;"
                            "padding-left: 13px;"
                            "padding-right: 13px;}");
        this->setIconSize(QSize(24, 24));
        this->setIcon(QIcon(":/images/myself/guanzhu.png"));
    }
    else
    {
        this->setText("关注");
        this->setStyleSheet("border-radius : 18px;"
                            "border : 1px solid #DDDDDD;"
                            "color : #999999;"
                            "font-size : 14px;");
        //取消图标
        this->setIcon(QIcon());
    }
}

将关注按钮类型提升为AttentionButton,就能看到按钮的效果。
在其他⽤⼾界⾯显⽰时,关注按钮上的⽂本刚开始并不⼀定是"关注",此处需要根据当前⽤⼾对视频上传⽤的实际关注情况来进⾏设置。

void MyselfWidget::getOtherUserInfoDone()
{
	//...
    //判断当前登录用户是否关注当前界面展示的用户
    ui->attentionBtn->changeAttentionStatus(otherUserInfo->isFollowing == 1);
    //...
}

完成之后,给关注按钮绑定槽函数,根据⽤⼾的点击发送关注或取消关注请求。
新增关注请求url:POST /HttpService/newAttention
请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
userId string 用户ID

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息

取消关注请求url:POST /HttpService/delAttention
请求参数

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客户端会话ID
userId string 用户ID

返回响应:200 OK

响应参数

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
//myselfwidget.h
    //当关注状态改变时触发
    void attentionChanged();
    void newAttentionDone();
    void delAttentionDone();
    private:
        Login* login = nullptr;
//myselfwidget.cpp
void MyselfWidget::attentionChanged()
{
    auto dataCenter = model::DataCenter::getInstance();
    if(dataCenter->getMyselfUserInfo()->isTempUser())
    {
        if(login == nullptr)
            login = new Login();
        Toast::showToast("还没有登录~",login);
        return;
    }
    bool isAttention = !ui->attentionBtn->attentionStatu();
    ui->attentionBtn->changeAttentionStatus(isAttention);
    auto otherUserInfo = dataCenter->getOtherUserInfo();
    if(isAttention)
    {
        dataCenter->newAttentionAsync(otherUserInfo->userId);
    }
    else
    {
        dataCenter->delAttentionAsync(otherUserInfo->userId);
    }
}

void MyselfWidget::newAttentionDone()
{
    //更新数据中心的数据同时更新界面中的数据
    auto dataCenter = model::DataCenter::getInstance();
    auto myselfInfo = dataCenter->getMyselfUserInfo();
    auto otherInfo = dataCenter->getOtherUserInfo();
    myselfInfo->followedCount++;
    otherInfo->followerCount++;
    ui->fansCountLabel->setText(intToString2(otherInfo->followerCount));
}

void MyselfWidget::delAttentionDone()
{
    //更新数据中心的数据同时更新界面中的数据
    auto dataCenter = model::DataCenter::getInstance();
    auto myselfInfo = dataCenter->getMyselfUserInfo();
    auto otherInfo = dataCenter->getOtherUserInfo();
    myselfInfo->followedCount--;
    otherInfo->followerCount--;
    ui->fansCountLabel->setText(intToString2(otherInfo->followerCount));
}

//datacenter.h
    //新关注-取消关注
    void newAttentionAsync(const QString& userId);
    void delAttentionAsync(const QString& userId);

//datacenter.cpp
void DataCenter::newAttentionAsync(const QString &userId)
{
    netClient.newAttention(userId);
}

void DataCenter::delAttentionAsync(const QString &userId)
{
    netClient.delAttention(userId);
}

//netclient.cpp
void NetClient::newAttention(const QString &userId)
{
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["userId"] = userId;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/newAttention",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        emit dataCenter->newAttentionDone();
        LOG() << "newAttention请求结束,服务端成功设置当前用户关注目标用户";
    });
}

void NetClient::delAttention(const QString &userId)
{
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["userId"] = userId;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/delAttention",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        emit dataCenter->delAttentionDone();
        LOG() << "delAttention请求结束,服务端成功取消设置当前用户关注目标用户";
    });
}

假服务端的处理逻辑:

//mockserver.cpp
    //客户端请求新增关注
    httpServer.route("/HttpService/newAttention",[=](const QHttpServerRequest& req){
        return this->newAttention(req);
    });
    //客户端请求取消关注
    httpServer.route("/HttpService/delAttention",[=](const QHttpServerRequest& req){
        return this->delAttention(req);
    });

QHttpServerResponse MockServer::newAttention(const QHttpServerRequest &request)
{
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[newAttention] 收到 newAttention 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "新增对对应用户的关注,目标用户Id为:" << reqData["userId"].toString();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

QHttpServerResponse MockServer::delAttention(const QHttpServerRequest &request)
{
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[delAttention] 收到 delAttention 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "取消对对应用户的关注,目标用户Id为:" << reqData["userId"].toString();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

更多推荐