【网络云盘客户端】——上传文件的功能的实现
目录
上传文件功能的实现
uploadtask的设计
设置上传的槽函数
uploadFileAction接口
uploadFile接口
定时上传文件
进度条的设计
上传文件功能的实现
上传文件功能实现
1.双击 ”上传文件 “的 QListWidgetItem 或者 点击 “上传” 菜单项 都会弹出一个文件对话框
2.在文件对话框选中对应的文件,保存所有文件的路径(在对话框中可以选中多个文件)
3.将对应的文件存放到 uploadtask* m_uploadtask中(uploadtask 是一个上传文件队列类型)
4.定义m_uploadTimer定时器,每0.5秒调用 uploadFileAction 进行上传文件。
5.uploadFileAction中先发送一个 md5 http请求给服务器
- 如果文件存在服务器,则 秒传成功,不需要将文件内容发送到服务器上
- 如果文件不存在服务器,则再发送一个upload的请求,将文件的内容上传到服务器上。
6.妙传失败,调用 uploadFile 接口,将文件内容上传到服务器上。
uploadtask的设计
1.UploadFileInfo 是上传文件的信息的结构体
#define UPLOAD_NOT 0 //未上传
#define UPLOADING 1 //正在上传
#define UPLOAD_FINISHED 2 //上传完成
#define UPLOAD_FAILD 3 //上传失败
#define UPLOAF_FILE_EXISTE 4 //上传的文件已存在struct UploadFileInfo
{QString md5; //文件的md5值QString fileName; //文件名称QString filePath; //文件路径qint64 size; //文件大小FileDataProgress *fdp; //进度条int uploadStatus; //0.未上传,1.正在上传,2.上传完成
};
2.UploadTask 是 上传任务 列表类,该 任务列表 存储的是即将上传的文件的信息 UploadFileInfo
//uploadtask.h文件//上传任务列表类, 单例模式
class UploadTask
{
public ://获取UploadTask唯一实例对象static UploadTask* getInstance();//添加文件到上传任务列表中int appendUploadTask(QString filePath);//判断任务列表是否为空bool isEmpty();//取出任务UploadFileInfo* takeTask();//删除任务void delUploadTask();//清空上传任务列表列表void clearList();
private:UploadTask();~UploadTask();//静态数据成员,类中声明,类外必须定义static UploadTask* m_instance;
private:QList<UploadFileInfo*> m_fileList;
};//uploadtask.cpp文件//添加文件到上传任务列表中
int uploadtask::appendUploadTask(QString filePath)
{QFileInfo* fileinfo=new QFileInfo(filePath);if(fileinfo->size()>30*1024*1024){//如果文件大于30mb,则放弃该文件delete fileinfo;return -1;}//新建一个UploadFileInfo对象UploadFileInfo* uploadFileInfo=new UploadFileInfo;uploadFileInfo->size=fileinfo->size();uploadFileInfo->fileName=fileinfo->fileName();uploadFileInfo->filePath=filePath;uploadFileInfo->md5=Common::getInstant()->getFileMd5(filePath);uploadFileInfo->uploadStatus=UPLOAD_NOT;//设置未上传状态filedataProgress* upProgress=new filedataProgress;upProgress->setName(uploadFileInfo->fileName);uploadFileInfo->fdp=upProgress;//将进度条插入到上传列表中uploadlayout::getInstant()->getVLayout()->insertWidget(0,upProgress);m_filelist.append(uploadFileInfo);
}//判断任务列表是否为空
bool uploadtask::isEmpty()
{return m_filelist.empty();
}//取出任务
UploadFileInfo* uploadtask::takeTask()
{if(m_filelist.empty()){return nullptr;}UploadFileInfo* temp=m_filelist.at(0);return temp;
}//删除任务
void uploadtask::delUploadTask()
{//1.取出任务,判断该任务的状态,如果是上传完成,上传失败,上传成功,该任务是会被删除掉//2.将该任务从列表中移除//3.删除进度条,并删除该任务的资源if(isEmpty()){return;}UploadFileInfo* temp=m_filelist.at(0);if(temp->uploadStatus==UPLOAD_FINISHED||temp->uploadStatus==UPLOAD_FAILD||temp->uploadStatus==UPLOAF_FILE_EXISTE ){//删除该任务qDebug()<<"删除任务";m_filelist.removeAt(0);//将进度条从布局中移除uploadlayout::getInstant()->getVLayout()->removeWidget(temp->fdp);delete temp->fdp;delete temp;}
}//清空任务列表
void uploadtask::clearList()
{int len=m_filelist.size();for(int i=0;i<len;i++){UploadFileInfo* temp=m_filelist.at(0);m_filelist.removeAt(0);uploadlayout::getInstant()->getVLayout()->removeWidget(temp->fdp);//将进度条从布局中移除delete temp->fdp;delete temp;}
}
设置上传的槽函数
点击 “上传文件" 的QListWidgetItem,将文件的上传任务 添加 到 上传队列中。
//设置“上传文件”QListWidgetItem的槽函数
//双击QListWidget窗口中的控件,则会发出一个itemDoubleClicked信号
connect(ui->listWidget,&QListWidget::itemDoubleClicked,this,[=](QListWidgetItem *item){if(item->text()=="上传文件"){addUploadFile();}});//设置“上传”菜单项的槽函数connect(m_uploadAction,&QAction::triggered,this,[=]{addUploadFile();qDebug()<<"上传";
});//添加上传文件
void myfile::addUploadFile()
{//getOpenFileNames可以获取多个文件的路径QStringList filelist=QFileDialog::getOpenFileNames();for(int i=0;i<filelist.size();i++){//将文件添加到m_uploadtask中int res=m_uploadtask->appendUploadTask(filelist[i]);if(res==-1){//服务器设置最多只能上传30m大小的文件QMessageBox::warning(this,"警告","文件大小大于30m");}}
}
uploadFileAction接口
- uploadFileAction从m_uploadtask获取一个UploadFileInfo对象
- 在根据UploadFileInfo对象信2息发送一个 HTTP请求给服务器去验证文件是否存在。
POST http://119.23.41.13:80/md5 HTTP/1.1
Content-Type: application/json{"fileName": "111.rtf", //文件名"md5": "8274425de767b30b2fff1124ab54abb5",//文件md5值,标识文件的唯一性"token": "7b4b4922958c5cbb153df2668b714144",//验证用户身份"user": "zhangsan"//用户信息
}
客户端根据服务器返回的 code 做出相应的动作。
"code":"005" //上传的文件已存在(别的用户正在上传该文件)
"code":"006" //秒传成功,文件已经存在
"code":"007" // 秒传失败,文件不存在,需要调用uploadFile上传文件,
"code":"011" //"Token 验证失败,身份过期
//获取m_uploadtask中的一个上传任务
void myfile::uploadFileAction()
{if(m_uploadtask->isEmpty()){//任务列表为空// qDebug()<<"任务列表为空";return;}UploadFileInfo* uploadFileInfo=m_uploadtask->takeTask();//先进行快传,判断文件是否已经上传成功//封装http请求//如果该文件没有上传,则发送请求if(uploadFileInfo->uploadStatus==UPLOAD_NOT){uploadFileInfo->uploadStatus=UPLOADING;QNetworkRequest request;//从配置文件中获取到ip地址和port端口号QString ip=Common::getInstant()->getConfValue("web_server","ip");QString port=Common::getInstant()->getConfValue("web_server","port");QString url = QString("http://%1:%2/md5").arg(ip).arg(port);request.setUrl(QUrl(url));//设置文件类型request.setHeader(QNetworkRequest::ContentTypeHeader,QVariant("application/json"));//将data数据以QJson的格式发送给服务器QJsonObject object;object.insert("user", m_logininfo->user());object.insert("token", m_logininfo->token());object.insert("md5",uploadFileInfo->md5);object.insert("fileName",uploadFileInfo->fileName);QJsonDocument doc(object);QByteArray data=doc.toJson();//发送请求// qDebug()<<data;QNetworkReply* rely=m_manager->post(request,data);connect(rely,&QNetworkReply::readyRead,this,[=]{//响应到达,读取所有的数据QByteArray s=rely->readAll();qDebug() << "服务器返回数据:" << QString(s);//将s数据转换为Json对象QJsonParseError err;QJsonDocument document=QJsonDocument::fromJson(s,&err);if(err.error!=QJsonParseError::NoError){qDebug()<<"QJson格式错误";return;}//将QJson字符串转换为QJson对象QJsonObject object1;object1=document.object();//获取状态码QString value1=object1["code"].toString();if(value1=="005"){qDebug()<<"文件已经存在";//文件正在上传uploadFileInfo->uploadStatus=UPLOAF_FILE_EXISTE;Common::getInstant()->writeRecord(m_logininfo->user(),uploadFileInfo->fileName,value1);m_uploadtask->delUploadTask();return;}if(value1=="006"){uploadFileInfo->uploadStatus=UPLOAF_FILE_EXISTE;Common::getInstant()->writeRecord(m_logininfo->user(),uploadFileInfo->fileName,value1);m_uploadtask->delUploadTask();qDebug()<<"妙传成功";}if(value1=="007"){//qDebug()<<"上传文件";//文件没有上传到服务器上,需要将文件上传到服务器上uploadFile(uploadFileInfo);}if(value1=="111"){Common::getInstant()->writeRecord(m_logininfo->user(),uploadFileInfo->fileName,value1);qDebug()<<"token验证失败";}});}
}
uploadFile接口
uploadFile是发送一个 http请求 将文件中数据发送给服务器,服务器会将其进行保存。
POST http://119.23.41.13:80/upload HTTP/1.1
Content-Type: application/json------WebKitFormBoundaryNr0Jm9D3w0GCiG9g //文件边界线
Content-Disposition: form-data; user="zhangsan" filename="111.rtf" md5="8274425de767b30b2fff1124ab54abb5" size=7
Content-Type: application/octet-stream文件数据
------WebKitFormBoundaryNr0Jm9D3w0GCiG9g //文件边界线
服务器返回值:
"code":"008"
"code":"009"
//将文件上传到服务器上
/*
------WebKitFormBoundaryDQAR0QX1ojAyzAre\r\n
Content-Disposition: form-data; name="file"; filename="logo.png"\r\n
Content-Type: image/png\r\n
\r\n
真正的文件内容\r\n
------WebKitFormBoundaryDQAR0QX1ojAyzAre
*/
//将文件内容上传到服务器上
void myfile::uploadFile(UploadFileInfo *uploadFileInfo)
{QFile file(uploadFileInfo->filePath);file.open(QIODevice::ReadOnly | QIODevice::Text);logininfoinstance *login = logininfoinstance::getInstant();//getBoundary接口是随机生成一个文件边界线QString boundary = m_common->getBoundary();QByteArray data;data.append(boundary);data.append("\r\n");data.append("Content-Disposition: form-data; ");data.append(QString("user=\"%1\" filename=\"%2\" md5=\"%3\" size=%4").arg(login->user()).arg(uploadFileInfo->fileName).arg(uploadFileInfo->md5).arg(uploadFileInfo->size));data.append("\r\n");data.append("Content-Type: application/octet-stream");data.append("\r\n");data.append("\r\n");//上传中的数据data.append(file.readAll());data.append("\r\n");data.append(boundary);//文件边界线if (file.isOpen()) {file.close();}QString url = QString("http://%1:%2/upload").arg(login->ip()).arg(login->port());QNetworkRequest request;request.setUrl(url);request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));//发送http请求QNetworkReply *reply = m_manager->post(request, data);if (reply == NULL) {qDebug() << "请求失败";return;}//显示文件上传进度connect(reply, &QNetworkReply::uploadProgress, this, [=](qint64 bytesSent, qint64 bytesTotal){//bytesSent 上传的字节数//bytesTotal 文件需要上传的总字节数if (bytesTotal != 0) {//显示进度条(设置进度条)uploadFileInfo->fdp->setValue(bytesSent, bytesTotal);}});connect(reply, &QNetworkReply::finished, this, [=](){//文件上传完成后if (reply->error() != QNetworkReply::NoError) {qDebug() << reply->errorString();} else {QByteArray s=reply->readAll();/*008: 上传成功009: 上传失败*///响应到达,读取所有的数据// qDebug() << "服务器返回数据:" << QString(s);//将s数据转换为Json对象QJsonParseError err;QJsonDocument document=QJsonDocument::fromJson(s,&err);if(err.error!=QJsonParseError::NoError){qDebug()<<"QJson格式错误";return;}//将QJson字符串转换为QJson对象QJsonObject object1;object1=document.object();//获取状态码QString value1=object1["code"].toString();if (value1== "008") {qDebug() << "上传成功";uploadFileInfo->uploadStatus=UPLOAD_FINISHED;getFileCount(Normal);} else if (value1 == "009") {uploadFileInfo->uploadStatus=UPLOAD_FAILD;qDebug() << "上传失败";}//将传输记录记录到文件中Common::getInstant()->writeRecord(login->user(),uploadFileInfo->fileName,value1);// writeRecord(QString user, QString fileName, QString code, QString path)}m_uploadtask->delUploadTask();reply->deleteLater();});
}
定时上传文件
定义一个定时器,定时器每0.5秒 调用一次uploadFileAction函数,
//设置每0.5秒调用uploadFileActionm_uploadTimer.start(500);connect(&m_uploadTimer,&QTimer::timeout,this,[=](){uploadFileAction();});
进度条的设计
自定义一个进度条类型filedataProgress ,包括filedataProgress.h ,filedataProgress.cpp,filedataProgress.ui 文件.
filedataProgress.ui界面设计:
//filedataProgress.cpp文件
//设置文件名
void filedataProgress::setName(QString name)
{ui->label->setText(name);
}
//设置进度条数据
void filedataProgress::setValue(int value,int maxValue)
{ui->progressBar->setValue(value*100/maxValue);
}
当上传任务添加到上传列表时,就将进度条显示到 上传列表中,等待客户端上传文件。
在uploadFile接口 中需要设置 QNetWorkReply的uploadProgress信号,将上传的进度显示到进度条上。。QNetWorkReply每 当上传一次文件数据,则会发出uploadProgress信号
注意: bytesTotal 不能为 0,如果为 0,则会崩溃。
//显示文件上传进度connect(reply, &QNetworkReply::uploadProgress, this, [=](qint64 bytesSent, qint64 bytesTotal){//bytesSent 上传的字节数//bytesTotal 文件需要上传的总字节数if (bytesTotal != 0) {//显示进度条(设置进度条)uploadFileInfo->fdp->setValue(bytesSent, bytesTotal);}});
相关文章:

【网络云盘客户端】——上传文件的功能的实现
目录 上传文件功能的实现 uploadtask的设计 设置上传的槽函数 uploadFileAction接口 uploadFile接口 定时上传文件 进度条的设计 上传文件功能的实现 上传文件功能实现 1.双击 ”上传文件 “的 QListWidgetItem 或者 点击 “上传” 菜单项 都会弹出一个文件对话框 2.在文…...

WebView2对比CefSharp的超强优势
第一次使用了CefSharp组件,集成开发结束后,测试及使用过程中遇到了一些无法处理的bug及严重的性能问题。然后又测试对比了其他多种组件,具体情况可以阅读我的博客 《.NET桌面程序集成Web网页开发的十种解决方案》。最终选用了微软新出…...

前端需要知道的计算机网络知识
1 Web 机制 无论通过有线方式 (通常是网线) 还是无线方式(比如 wifi 或蓝牙),通信需要进行连接,网络上的每台计算机需要链接到路由器(router)。 路由器确保从一台计算机上发出的一条信息可以到达正确的计算机。计算机…...
[2023杭电多校5 1005] Snake (生成函数)
题意 有 n n n 个标号为 1 , 2 , ⋯ , n 1,2,\cdots,n 1,2,⋯,n 的球,放到 m m m 个无标号盒子 (盒内顺序有标号),且每个盒子球数不超过 k k k,求方案数对 998 244 353 998\,244\,353 998244353 取模。 1 ≤ m , k ≤ n ≤ 1 0 6 1 \le…...

【MyBtis】各种查询功能
目录 【MyBtis】配置和映射 11.1 示例:实现表数据的增、删、改、查 1.创建工程mybatis_DML demo 2.创建数据库操作的工具类:DBOperatorMgr.java 3.创建映射接口 4.创建XML映射文件 5.测试 【MyBtis】配置和映射 MyBatis 的真正强大之外在于它的映射语句…...

H5打包封装小程序系统开发
H5打包封装小程序系统开发 H5打包封装小程序系统开发是指将H5页面打包封装成小程序的开发过程。下面是一个简单的步骤: 准备工作:首先,需要准备好H5页面的代码和资源文件。确保H5页面在浏览器中正常运行,并且没有依赖于浏览器特…...

SpringBoot集成jasypt,加密yml配置文件
SpringBoot集成jasypt,加密yml配置文件 一、pom配置二、生成密文代码三、配置3.1、yml加密配置3.2、密文配置3.3、启动配置3.4、部署配置 四、遇到的一些坑 最新项目安全检测,发现配置文件中数据库密码,redis密码仍处理明文状态 一、pom配置…...

【C++】模板(初阶)
1、泛型编程 泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础 2、函数模板 函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本…...

windows下的txt文档,传到ubuntu后,每行后面出现^M,怎么处理?
问题背景:windows下pycharm生成的txt文档,传到ubuntu后,每行后面出现^M 用vim打开显示 使用cat -A filename显示如下 参考https://www.lmlphp.com/user/16697/article/item/579325/给出的几种方法 方法一、dos2unix filename。服务器没装…...

LabVIEW FPGA开发实时滑动摩擦系统
LabVIEW FPGA开发实时滑动摩擦系统 由于非线性摩擦效应的建模和补偿的固有困难,摩擦系统的运动控制已被广泛研究。最近,人们更加关注滑动动力学和滑动定位,作为传统机器人定位的低成本和更灵活的驱动替代方案。摩擦控制器设计和适当选择基础…...

Prometheus服务器、Prometheus被监控端、Grafana、Prometheus服务器、Prometheus被监控端、Grafana
day03 day03Prometheus概述部署Prometheus服务器环境说明:配置时间安装Prometheus服务器添加被监控端部署通用的监控exporterGrafana概述部署Grafana展示node1的监控信息监控MySQL数据库配置MySQL配置mysql exporter配置mysql exporter配置prometheus监控mysql自动…...
常见的锁策略(面试八股文)
1.乐观锁vs悲观锁 乐观锁:预测该场景中不太会出现锁冲突的情况。(后续做的工作会更少) 悲观锁:预测该场景非常容易出现锁冲突(后续做的工作会更多) 锁冲突:多个线程同时尝试去获得同一把锁&…...
SO_KEEPALIVE、TCP_KEEPIDLE、TCP_KEEPINTVL、保活包
SO_KEEPALIVE SO_KEEPALIVE 是一个套接字选项,用于设置是否启用 keepalive 机制。在这段代码中没有涉及到 SO_KEEPALIVE 选项的设置。 当 SO_KEEPALIVE 被设置为非零值时,表示启用 keepalive 机制。keepalive 是一种用于检测连接是否仍然有效的机制。通…...

【phaser微信抖音小游戏开发005】画布上添加图片
特别注意:真机模拟的时候,尽量使用网络图片资源,不要在小程序源文件里面使用图片,会出现真机加载不成功,小程序包体积过大的问题。我们学习过程中,只是作为演示使用。 推荐使用场景: 背景图片…...

【设计模式——学习笔记】23种设计模式——外观模式Facade(原理讲解+应用场景介绍+案例介绍+Java代码实现)
文章目录 案例引入介绍基本介绍类图出场角色 案例实现案例一类图代码实现 案例二类图代码实现 外观模式在Mybatis源码中的应用总结文章说明 案例引入 在家庭影院中,要享受一场电影,需要如下步骤: 直接用遥控器:统筹各设备开关开…...
消息队列 -提供上层服务接口
目录 前言封装数据库封装内存操作内存的设计思想 应答模式 代码实现测试代码 前言 我们之前已经将 数据库 的操作 和文件的操作 都完成了, 但是对于上层调用来说, 并不关心是于数据库中存储数据还是往文件中存储数据, 因此 我们提供一个类, 封装一下 上述俩个类中的操作, 并将…...

maven引入本地jar包的简单方式【IDEA】【SpringBoot】
前言 想必点进来看这篇文章的各位,都是已经习惯了Maven从中央仓库或者阿里仓库直接拉取jar包进行使用。我也是🤡🤡。 前两天遇到一个工作场景,对接三方平台,结果对方就是提供的一个jar包下载链接,可给我整…...

【爬虫逆向案例】某易云音乐(评论)js逆向—— params、encSecKey解密
声明:本文只作学习研究,禁止用于非法用途,否则后果自负,如有侵权,请告知删除,谢谢! 【爬虫逆向案例】某易云音乐(评论)js逆向—— params、encSecKey解密 1、前言2、行动…...

【uni-app】【Android studio】手把手教你运行uniapp项目到Android App
运行到Android App基座 选择运行到Android App基座 选择运行项目 1、连接手机,在手机上选择 传输文件。 2、打开 设置-> 关于本机 -> 版本信息->连续点击4-5次版本号 ,输入手机密码,系统就进入了开发者模式。 3、设置 > 其他设…...

多线程(JavaEE初阶系列6)
目录 前言: 1.什么是线程池 2.标准库中的线程池 3.实现线程池 结束语: 前言: 在上一节中小编带着大家了解了一下Java标准库中的定时器的使用方式并给大家实现了一下,那么这节中小编将分享一下多线程中的线程池。给大家讲解一…...

【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...

Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
Cesium1.95中高性能加载1500个点
一、基本方式: 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...

苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...

相机从app启动流程
一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...