Qt u盘自动升级软件

Qt u盘自动升级软件

  • [Chapter1 Qt u盘自动升级软件](#Chapter1 Qt u盘自动升级软件)
    • u盘自动升级软件思路:
    • [step1. 获取U盘 判断U盘名字是否正确, 升级文件是否存在。](#step1. 获取U盘 判断U盘名字是否正确, 升级文件是否存在。)
    • [step2. 升级](#step2. 升级)
    • [step3. 升级界面](#step3. 升级界面)
  • [Chapter2 Qt 嵌入式设备应用程序,通过U盘升级的一种思路](#Chapter2 Qt 嵌入式设备应用程序,通过U盘升级的一种思路)
  • [Chapter3 在开发板上运行的QT应用程序,如何拷贝文件到U盘](#Chapter3 在开发板上运行的QT应用程序,如何拷贝文件到U盘)
  • [Chapter4 嵌入式Qt,U盘升级程序](#Chapter4 嵌入式Qt,U盘升级程序)

Chapter1 Qt u盘自动升级软件

原文链接

u盘自动升级软件思路:

1.检测U盘是否存在

2.检测升级文件是否存在,升级文件版本是否比当前软件版本新

3.软件升级

step1. 获取U盘 判断U盘名字是否正确, 升级文件是否存在。

cpp 复制代码
//获取U盘  判断U盘名字是否正确, 升级文件是否存在。
void MainWindow::getUDisk()
{
    QMap<QString,QString> namePath;
    foreach (const QStorageInfo &storage, QStorageInfo::mountedVolumes())
    {
        if (storage.isValid() && storage.isReady())
        {
            UDiskPath = storage.rootPath();
            namePath.insert(storage.displayName(),storage.rootPath());
        }
    }
    QString path = namePath.value("LADYBUG");
    QFile file(path+"/"+app::updateFile);
    if(file.exists())
    {
        //存在 弹窗是否升级
    }
    else
    {
        //不存在
    }
}

step2. 升级

cpp 复制代码
    OperateThread *thread = new OperateThread;
    thread->setUpgradeFile(strSrcFile);
    ShowProgressDialog dialog(this);
 
    connect(thread, SIGNAL(emitFinish(int)), &dialog, SLOT(onThreadFinished(int)));
 
    thread->start();
 
    dialog.exec();

step3. 升级界面

cpp 复制代码
class ShowProgressDialog : public QDialog
{
    Q_OBJECT
public:
    explicit ShowProgressDialog(QWidget *parent = 0);
    ~ShowProgressDialog();
 
    void setTitleText(const QString &strText);
    void setMessageText(const QString &strMsg);
 
public slots:
    void on_btnOk_clicked();
    void onThreadFinished(int nExitCode);
 
private:
    QLabel          *m_labelTitle;
    QLabel          *m_labelMsg;
    QPushButton     *m_btnOk;
    QLabel          *m_labelWait;
    QMovie          *m_pMovie;
};
 
ShowProgressDialog::ShowProgressDialog(QWidget *parent) : QDialog(parent)
{
    setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Dialog);
    setAttribute(Qt::WA_X11DoNotAcceptFocus);
    setFocusPolicy(Qt::NoFocus);
    //更改背景色
    QPalette palette = this->palette();
    QPixmap pix(":/res/dialog_bg.png");
    palette.setBrush(QPalette::Background,QBrush(pix));
    setAutoFillBackground(true);
    this->setPalette(palette);
 
    resize(410, 260);
    setMinimumSize(QSize(410, 260));
    setMaximumSize(QSize(410, 260));
 
    m_labelTitle = new QLabel(this);
    m_labelMsg = new QLabel(this);
    m_btnOk = new QPushButton(this);
    m_labelWait = new QLabel(this);
    m_pMovie = new QMovie(":/res/loading.gif");
 
    m_labelTitle->setGeometry(QRect(10, 1, 220, 30));
    m_labelMsg->setGeometry(QRect(30, 60, 350, 60));
    m_labelMsg->setWordWrap(true);
    m_btnOk->setGeometry(QRect(320, 180, 58, 58));
    m_labelWait->setGeometry(QRect(30, 120, 322, 18));
 
    m_labelWait->setMovie(m_pMovie);
    m_pMovie->start();
 
    m_labelTitle->setStyleSheet("font: bold 17px; color: white;");
    m_labelMsg->setStyleSheet("font: bold 17px ; color: black; ");
 
    m_btnOk->setStyleSheet("QPushButton{background-image: url(:/res/dialog_ok.png);border: 0px;}"
                           "QPushButton:pressed{background-image: url(:/res/dialog_ok_p.png);border: 0px;}");
 
    connect(m_btnOk , SIGNAL(clicked()) , this , SLOT(on_btnOk_clicked()));
 
    setTitleText("升级");
    setMessageText("正在升级,请勿插拔U盘!");
    m_btnOk->hide();
}
 
ShowProgressDialog::~ShowProgressDialog()
{
    delete m_labelTitle;
    delete m_labelMsg;
    delete m_btnOk;
}
 
void ShowProgressDialog::setTitleText(const QString &strText)
{
    m_labelTitle->setText(strText);
}
 
void ShowProgressDialog::setMessageText(const QString &strMsg)
{
    m_labelMsg->setText(strMsg);
}
 
void ShowProgressDialog::on_btnOk_clicked()
{
    QDialog::accept();
}
 
void ShowProgressDialog::onThreadFinished(int nExitCode)
{
    qDebug() << "nExitCode" << nExitCode;
    if (nExitCode == 0)
    {
        setMessageText("升级成功,请重新上电。");
        m_btnOk->show();
        m_pMovie->stop();
    }
    else
    {
        setMessageText("升级出错!请断电以恢复!");
        m_btnOk->show();
        m_pMovie->stop();
    }
}

注意:线程,继承自QThread,完成复制工作。使用QProcess来完成Linux下的解压和删除的工作。关于Qt的多线程,可以去查找一下其他的教程,这里就不做过多的解释。

cpp 复制代码
class OperateThread : public QThread
{
    Q_OBJECT
public:
    explicit OperateThread(QObject *parent = 0);
 
public:
    void setUpgradeFile(QString strUpgradeFile)
    {
        m_UpgradeFile = strUpgradeFile;
    }
 
protected:
    void run();
 
signals:
    void emitFinish(int);
 
public slots:
 
private:
    void OprUpgradeUp();
 
private:
    QString  m_UpgradeFile;
};
 
OperateThread::OperateThread(QObject *parent) : QThread(parent)
{
 
}
 
void OperateThread::run()
{
    //很复杂的数据处理
    OprUpgradeUp();
}
 
void OperateThread::OprUpgradeUp()
{
#define DEST_DIR    "../"
 
    QString destDir = QString("%1/update.tar.gz").arg(DEST_DIR);
 
    if( QFile::copy(m_UpgradeFile, destDir) )
    {
        qDebug()<<"-------成功-----------";
    }
    else
    {
        qDebug()<<"-------出错-----------";
        //升级失败
        emit emitFinish(1);
        return;
    }
 
#ifdef Q_OS_LINUX
    QString exe = QString("tar -zxvf %1 -C %2").arg(destDir).arg(DEST_DIR);
    QProcess::execute(exe);
 
    //升级成功,自动同步磁盘并删除ARM上的升级包
    exe = QString("sync");
    QProcess::execute(exe);
    exe = QString("rm -rf %1").arg(destDir);
    QProcess::execute(exe);
#endif
    //升级成功
    emit emitFinish(0);
}

Chapter2 Qt 嵌入式设备应用程序,通过U盘升级的一种思路

原文链接:https://blog.csdn.net/qq1113231395/article/details/81867153

最近在做一个通过U盘升级的功能,程序是运行在ARM Linux Qt平台上的。这个应该是很多嵌入式设备必备的一个功能了,所以把这部分的实现抽出来,做成一个例子供需要的人参考。这只是U盘升级的一种思路,如果有更好的方法,也可以提供相应的意见。

源码下载:softwareupgrade.tar.gz

升级文件的格式是通过tar压缩后的文件以gz结尾的, 可以通过tar命令生成相应的升级文件如update.tar.gz:

bash 复制代码
tar -czvf update.tar.gz demo

主要思路就是,点击相应的功能后从U盘中选中需要升级的文件update.tar.gz,之后开启一个线程和一个提示正在升级的对话框,以免让用户觉得假死。在线程中完成的事情是:将选中的升级文件复制到应用程序的目录中,然后将update.tar.gz 解压覆盖原来的程序。最后将update.tar.gz删除。发送一个线程结束的信号。

下面就介绍一下实现过程。

MainWindow很简单,只有一个按钮。 点击按钮开启线程,显示提示框,绑定线程结束后的信号。提示框根据信号的参数值来确定升级是否成功。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}
 
MainWindow::~MainWindow()
{
    delete ui;
}
 
void MainWindow::on_pushButton_clicked()
{
    QString strSrcFile = QFileDialog::getOpenFileName(this, "选择升级文件", ".", "tar (*.gz)");
    qDebug() << "strSrcFile" << strSrcFile;
 
    OperateThread *thread = new OperateThread;
    thread->setUpgradeFile(strSrcFile);
    ShowProgressDialog dialog(this);
 
    connect(thread, SIGNAL(emitFinish(int)), &dialog, SLOT(onThreadFinished(int)));
 
    thread->start();
 
    dialog.exec();
}

运行的效果图:

提示框: void onThreadFinished(int nExitCode) 线程结束后更新提示框。里面还使用到了一些qss,来设置背景。

cpp 复制代码
class ShowProgressDialog : public QDialog
{
    Q_OBJECT
public:
    explicit ShowProgressDialog(QWidget *parent = 0);
    ~ShowProgressDialog();
 
    void setTitleText(const QString &strText);
    void setMessageText(const QString &strMsg);
 
public slots:
    void on_btnOk_clicked();
    void onThreadFinished(int nExitCode);
 
private:
    QLabel          *m_labelTitle;
    QLabel          *m_labelMsg;
    QPushButton     *m_btnOk;
    QLabel          *m_labelWait;
    QMovie          *m_pMovie;
};
 
ShowProgressDialog::ShowProgressDialog(QWidget *parent) : QDialog(parent)
{
    setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Dialog);
    setAttribute(Qt::WA_X11DoNotAcceptFocus);
    setFocusPolicy(Qt::NoFocus);
    //更改背景色
    QPalette palette = this->palette();
    QPixmap pix(":/res/dialog_bg.png");
    palette.setBrush(QPalette::Background,QBrush(pix));
    setAutoFillBackground(true);
    this->setPalette(palette);
 
    resize(410, 260);
    setMinimumSize(QSize(410, 260));
    setMaximumSize(QSize(410, 260));
 
    m_labelTitle = new QLabel(this);
    m_labelMsg = new QLabel(this);
    m_btnOk = new QPushButton(this);
    m_labelWait = new QLabel(this);
    m_pMovie = new QMovie(":/res/loading.gif");
 
    m_labelTitle->setGeometry(QRect(10, 1, 220, 30));
    m_labelMsg->setGeometry(QRect(30, 60, 350, 60));
    m_labelMsg->setWordWrap(true);
    m_btnOk->setGeometry(QRect(320, 180, 58, 58));
    m_labelWait->setGeometry(QRect(30, 120, 322, 18));
 
    m_labelWait->setMovie(m_pMovie);
    m_pMovie->start();
 
    m_labelTitle->setStyleSheet("font: bold 17px; color: white;");
    m_labelMsg->setStyleSheet("font: bold 17px ; color: black; ");
 
    m_btnOk->setStyleSheet("QPushButton{background-image: url(:/res/dialog_ok.png);border: 0px;}"
                           "QPushButton:pressed{background-image: url(:/res/dialog_ok_p.png);border: 0px;}");
 
    connect(m_btnOk , SIGNAL(clicked()) , this , SLOT(on_btnOk_clicked()));
 
    setTitleText("升级");
    setMessageText("正在升级,请勿插拔U盘!");
    m_btnOk->hide();
}
 
ShowProgressDialog::~ShowProgressDialog()
{
    delete m_labelTitle;
    delete m_labelMsg;
    delete m_btnOk;
}
 
void ShowProgressDialog::setTitleText(const QString &strText)
{
    m_labelTitle->setText(strText);
}
 
void ShowProgressDialog::setMessageText(const QString &strMsg)
{
    m_labelMsg->setText(strMsg);
}
 
void ShowProgressDialog::on_btnOk_clicked()
{
    QDialog::accept();
}
 
void ShowProgressDialog::onThreadFinished(int nExitCode)
{
    qDebug() << "nExitCode" << nExitCode;
    if (nExitCode == 0)
    {
        setMessageText("升级成功,请重新上电。");
        m_btnOk->show();
        m_pMovie->stop();
    }
    else
    {
        setMessageText("升级出错!请断电以恢复!");
        m_btnOk->show();
        m_pMovie->stop();
    }
}

线程,继承自QThread,完成复制工作。使用QProcess来完成Linux下的解压和删除的工作。关于Qt的多线程,可以去查找一下其他的教程,这里就不做过多的解释。

cpp 复制代码
class OperateThread : public QThread
{
    Q_OBJECT
public:
    explicit OperateThread(QObject *parent = 0);
 
public:
    void setUpgradeFile(QString strUpgradeFile)
    {
        m_UpgradeFile = strUpgradeFile;
    }
 
protected:
    void run();
 
signals:
    void emitFinish(int);
 
public slots:
 
private:
    void OprUpgradeUp();
 
private:
    QString  m_UpgradeFile;
};
 
OperateThread::OperateThread(QObject *parent) : QThread(parent)
{
 
}
 
void OperateThread::run()
{
    //很复杂的数据处理
    OprUpgradeUp();
}
 
void OperateThread::OprUpgradeUp()
{
#define DEST_DIR    "../"
 
    QString destDir = QString("%1/update.tar.gz").arg(DEST_DIR);
 
    if( QFile::copy(m_UpgradeFile, destDir) )
    {
        qDebug()<<"-------成功-----------";
    }
    else
    {
        qDebug()<<"-------出错-----------";
        //升级失败
        emit emitFinish(1);
        return;
    }
 
#ifdef Q_OS_LINUX
    QString exe = QString("tar -zxvf %1 -C %2").arg(destDir).arg(DEST_DIR);
    QProcess::execute(exe);
 
    //升级成功,自动同步磁盘并删除ARM上的升级包
    exe = QString("sync");
    QProcess::execute(exe);
    exe = QString("rm -rf %1").arg(destDir);
    QProcess::execute(exe);
#endif
    //升级成功
    emit emitFinish(0);
}

Chapter3 在开发板上运行的QT应用程序,如何拷贝文件到U盘

原文链接:https://blog.csdn.net/pang_fighting/article/details/139114780

笔者最近一直被这个问题所困惑,好在今天已经解决,之前找了很多资料,试了其他家的使用QProcess类来实现QT应用下的命令行实现,但很遗憾还是不行,找到新的解决方法如下:

bash 复制代码
system("cp -r /home/root/test_data /run/media/sda1");
system("sync");
system("umount /run/media/sda1");

解释一下,这需要你的开发板移植的系统能够插入U盘直接挂载,否则需要先检测U盘插入再挂载U盘,才可以使用,笔者用的正点原子157开发板,使用的是他们的官方系统,这个系统是支持插入U盘直接挂载的,挂载目录是"/run/media/sda1"。

U盘挂载完成之后,使用C函数system来执行命令行拷贝文件,拷贝完成之后执行sync命令,最后直接取消挂载拔出U盘即可!

Chapter4 嵌入式Qt,U盘升级程序

原文链接:https://blog.csdn.net/sjd753/article/details/135284869

前言

近期在完成一个U盘升级功能,对于Arm嵌入式设备来说应该算是必备的了。

参考链接:

https://blog.csdn.net/newnewman80/article/details/8766657

https://blog.csdn.net/newnewman80/article/details/8766657

一、实现过程

1.检测U盘中的更新程序

结合netlink捕获USB的热拔插,我们需要去检测U盘的挂载点,通常情况下都是挂载在/media下。

cpp 复制代码
bool checkConsoleAppExists() 
{
    DIR *dir = opendir("/run/media"); // 打开 /run/media 目录 具体的挂载点根据设备决定
    if (dir) 
    {
        struct dirent *entry;
        while ((entry = readdir(dir)) != NULL) 
        {
            if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) 
            {
                devpath = "/run/media/" + std::string(entry->d_name);
                path = "/run/media/" + std::string(entry->d_name) + "/updateFileName";
 
                std::string signatureFilePath = devpath +"/sigFileName.sig"; 
                std::string publicKeyPath = devpath + "/key.pub" ;
 
                if (access(path.c_str(), F_OK) != -1) 
                {
                    //检测到更新程序,可以做一些校验,我这里的话是验证数字签名,根据具体情况决定
                    return checkUpdate(signatureFilePath,publicKeyPath,path);
                }
            }
        }
        closedir(dir);
    }
    return false;
}

2. 数字验证

cpp 复制代码
/*检测是否需要更新,是否为正确的更新程序*/
bool checkUpdate(const std::string& signatureFile, const std::string& publicKeyFile, const std::string& dataFile) {
    // 打开更新文件
    // 这里是想要去检测版本号,去判断是否需要升级的,后面由于使用了数字签名就没有实现这部分。
    FILE *updateFile = fopen(dataFile.c_str(), "rb");
    if (!updateFile) {
        perror("Error opening update file\n");
        return false;
    }
    // 验证数字签名
    return verifySignature(signatureFile,publicKeyFile,dataFile);
}
cpp 复制代码
// 函数用于验证数字签名是否有效
bool verifySignature(const std::string& signatureFile, const std::string& publicKeyFile, const std::string& dataFile) 
{
    // 打开公钥文件以读取公钥信息
    FILE* fp = fopen(publicKeyFile.c_str(), "r");
    if (!fp) {
        std::cerr << "Error opening public key file." << std::endl;
        return false;
    }
 
    // 从公钥文件中读取 RSA 公钥信息
    RSA* rsa = PEM_read_RSA_PUBKEY(fp, NULL, NULL, NULL);
    fclose(fp);
 
    // 检查是否成功读取公钥信息
    if (!rsa) {
        std::cerr << "Error reading public key." << std::endl;
        return false;
    }
 
    // 创建一个用于存储 RSA 公钥的 EVP_PKEY 对象
    EVP_PKEY* evpKey = EVP_PKEY_new();
 
    // 将 RSA 公钥赋值给 EVP_PKEY 对象
    if (!EVP_PKEY_assign_RSA(evpKey, rsa)) {
        std::cerr << "Error assigning RSA key." << std::endl;
        RSA_free(rsa);
        return false;
    }
 
    // 创建一个用于消息摘要计算的 EVP_MD_CTX 对象
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    if (!ctx) {
        std::cerr << "Error creating context." << std::endl;
        RSA_free(rsa);
        return false;
    }
 
    // 打开数据文件以进行签名验证
    std::ifstream fileStream(dataFile, std::ios::binary | std::ios::ate);
    if (!fileStream.is_open()) {
        std::cerr << "Error opening data file." << std::endl;
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
 
    // 读取数据文件的大小和内容
    std::streamsize fileSize = fileStream.tellg();
    fileStream.seekg(0, std::ios::beg);
    std::vector<unsigned char> fileData(fileSize);
    if (!fileStream.read(reinterpret_cast<char*>(fileData.data()), fileSize)) {
        std::cerr << "Error reading data file." << std::endl;
        fileStream.close();
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
    fileStream.close();
 
    // 打开签名文件以进行签名验证
    std::ifstream signatureFileStream(signatureFile, std::ios::binary);
    if (!signatureFileStream.is_open()) {
        std::cerr << "Error opening signature file." << std::endl;
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
 
    // 读取签名文件的大小和内容
    signatureFileStream.seekg(0, std::ios::end);
    size_t signatureFileSize = signatureFileStream.tellg();
    signatureFileStream.seekg(0, std::ios::beg);
 
    std::vector<unsigned char> signatureData(signatureFileSize);
    if (!signatureFileStream.read(reinterpret_cast<char*>(signatureData.data()), signatureFileSize)) {
        std::cerr << "Error reading signature file." << std::endl;
        signatureFileStream.close();
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
    signatureFileStream.close();
 
    // 更新消息摘要的内容,计算数据文件的哈希值
    if (!EVP_VerifyUpdate(ctx, fileData.data(), fileSize)) {
        std::cerr << "Error updating verification." << std::endl;
        EVP_MD_CTX_free(ctx);
        RSA_free(rsa);
        return false;
    }
 
    // 验证签名的有效性
    int result = EVP_VerifyFinal(ctx, signatureData.data(), signatureFileSize, evpKey);
    EVP_MD_CTX_free(ctx);
    RSA_free(rsa);
 
    // 根据验证结果返回相应的布尔值
    if (result != 1) {
        std::cerr << "Signature verification failed." << std::endl;
        return false;
    }
    std::cout << "Signature verification successful." << std::endl;
    return true;
}

3.升级程序

升级程序的话,实际上就是一个拷贝的过程。将设备上的备份,U盘中的更新程序拷贝出来。

cpp 复制代码
/* 更新程序 */
bool copyFile(const char *sourcePath, const char *destinationPath) {
    
    //system("mv updateFileName  updateFileName.back"); //这里是一个备份,后续可以进行一个升级失败,备份还原。
 
    FILE *sourceFile = fopen(sourcePath, "rb");
    FILE *destinationFile = fopen(destinationPath, "wb");
 
    if (sourceFile == NULL) {
        printf("Error opening sourceFile files.\n");
        return false;
    }
    if(destinationFile == NULL){
        printf("Error opening  destinationFile files.\n");
        return false;
    }
    char buffer[1024];
    size_t bytesRead;
    while ((bytesRead = fread(buffer, 1, sizeof(buffer), sourceFile)) > 0) {
        printf("start copy file........................\n");
        fwrite(buffer, 1, bytesRead, destinationFile);
    }
 
    /*更简单一点就是用system命令,直接拷贝
      不过有一点需要注意,程序在运行的时候,有时候会拷贝失败。
      可以先用system命令删除或者备份,然后再进行拷贝,再用system给个权限。
    */
 
    fclose(sourceFile);
    fclose(destinationFile);
    return true;
}
 

4.主函数

cpp 复制代码
int main(void)
{
    struct sockaddr_nl client;
    struct timeval tv;
    int CppLive, rcvlen, ret;
    fd_set fds;
    int buffersize = 1024;
    CppLive = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
    memset(&client, 0, sizeof(client));
    client.nl_family = AF_NETLINK;
    client.nl_pid = getpid();
    client.nl_groups = 1; /* receive broadcast message*/
    setsockopt(CppLive, SOL_SOCKET, SO_RCVBUF, &buffersize, sizeof(buffersize));
    bind(CppLive, (struct sockaddr*)&client, sizeof(client));
    while (1) {
        char buf[UEVENT_BUFFER_SIZE] = { 0 };
        FD_ZERO(&fds);
        FD_SET(CppLive, &fds);
        tv.tv_sec = 0;
        tv.tv_usec = 100 * 1000;
        ret = select(CppLive + 1, &fds, NULL, NULL, &tv);
        if(ret < 0)
            continue;
        if(!(ret > 0 && FD_ISSET(CppLive, &fds)))
            continue;
        /* receive data */
        rcvlen = recv(CppLive, &buf, sizeof(buf), 0);
        if (rcvlen > 0) { 
            printf("%s\n", buf);
            //检测更新程序
            //升级程序
            //重启....
        }
    }
    close(CppLive);
    return 0;
}

二、总结

以上就是今天要讲的内容,本文仅仅简单介绍了结合netlink的u盘升级,后期可以通过Qt实现U盘升级,实现一个程序,通过界面可以判断更新状态。

相关推荐
曹牧17 分钟前
Oracle数据库中,将JSON字符串转换为多行数据
数据库·oracle·json
被摘下的星星37 分钟前
MySQL count()函数的用法
数据库·mysql
末央&1 小时前
【天机论坛】项目环境搭建和数据库设计
java·数据库
徒 花1 小时前
数据库知识复习07
数据库·作业
素玥1 小时前
实训5 python连接mysql数据库
数据库·python·mysql
jnrjian1 小时前
text index 查看index column index定义 index 刷新频率 index视图
数据库·oracle
瀚高PG实验室1 小时前
审计策略修改
网络·数据库·瀚高数据库
言慢行善2 小时前
sqlserver模糊查询问题
java·数据库·sqlserver
韶博雅2 小时前
emcc24ai
开发语言·数据库·python
有想法的py工程师2 小时前
PostgreSQL 分区表排序优化:Append Sort 优化为 Merge Append
大数据·数据库·postgresql