20242817李臻-安全文件传输系统-项目验收

安全文件传输系统项目报告

项目概述

本实验旨在设计并实现一个完整的安全文件管理系统,基于SM2+SM3+SM4混合密码体系,构建了一个具备高安全性的C/S架构文件传输平台。项目采用C/S架构,使用Qt框架开发,满足Linux系统调用、Socket网络编程、多线程处理等技术要求。

开发环境

  • 处理器:x86_64架构处理器
  • 操作系统:Ubuntu 20.04
  • 开发框架:Qt5
  • 数据库:SQLite 3
  • 密码库:gmssl

Gitee地址:

https://gitee.com/li-zhen1215/homework/tree/master/Secure File Transfer System

一、 功能完成以及完善情况

系统流程图

用户登录

系统采用登录验证机制作为入口。用户启动客户端后,首先进入登录界面:

  • 账号验证:
    • 若用户尚未注册账号,系统将引导其完成注册流程,创建合法用户信息;
    • 若用户已有账号,则可直接输入凭证完成身份验证。

进入主界面

登录成功后,用户将进入系统主界面,该界面集成了全部核心功能模块,。

文件管理功能

主界面提供了一系列文件传输和管理功能,具体包括:

  • 文件上传:用户可将本地文件上传至服务器;
  • 文件下载:可从服务器下载所需文件到本地;
  • 文件删除:允许用户删除自己上传的文件;
  • 刷新列表:实时刷新当前文件列表,查看最新文件信息;
  • 下载日志:支持用户查看文件下载历史记录,实现基本的审计追踪。

注销与退出

用户完成所需操作后,可选择注销账号,安全退出系统,确保账号信息和数据传输的安全性。

系统特点

  • 支持多用户并发访问:通过多线程/多连接机制,系统可同时处理多个用户请求;
  • 审计功能完善:系统记录用户操作日志,有助于行为追踪和系统安全审查。

1、总体架构完成情况

C/S架构实现

  • 服务器端 : ServerCore类继承QTcpServer,实现多线程并发处理
  • 客户端: Qt GUI应用程序,提供用户友好的图形界面
  • 网络通信: 基于TCP Socket的可靠数据传输

系统架构图


2 、核心模块架构

2.1 客户端架构

cpp 复制代码
Client/
├── MainWindow              // 主界面管理
├── LoginWindow            // 登录界面
├── RegisterWindow         // 注册界面
├── FileTransfer           // 文件传输核心
├── SecurityManager        // 密码学操作
└── AuditLogger           // 审计日志

2.2 服务器架构

cpp 复制代码
Server/
├── ServerCore             // 服务器核心 (继承QTcpServer)
├── ClientHandler          // 客户端连接处理
├── Database               // 数据库操作
├── SecurityManager        // 服务器密码学管理
└── AuditLogger           // 服务器审计系统

3、功能完成以及完善情况

3.1 用户管理系统

当用户首次使用系统时,需要进行注册。用户名和口令是进入系统的凭证,用户名用作实现访问控制功能,而口令作为验证用户身份的唯一凭证,只有口令和用户名相对应,用户才能正确进入系统。

3.1.1 用户注册功能

实现思路

  • 用户输入用户名和密码
  • 客户端发送注册请求到服务器
  • 服务器进行SM3密码哈希处理
  • 存储到SQLite数据库

为确保身份鉴别的安全性,口令仅保存经过 SM3 哈希算法计算后的哈希值,并采用加盐机制以增强抗攻击能力。
核心实现代码

cpp 复制代码
// 密码哈希处理 - SecurityManager类
QByteArray SecurityManager::hashPasswordWithSalt(const QString& password, const QByteArray& salt) {
    QByteArray actualSalt = salt.isEmpty() ? generateSalt() : salt;
    QByteArray combined = password.toUtf8() + actualSalt;
    
    // 10000轮SM3哈希运算 (PBKDF2-SM3)
    QByteArray hash = combined;
    const int rounds = 10000;
    
    for (int i = 0; i < rounds; i++) {
        SM3_CTX ctx;
        sm3_init(&ctx);
        sm3_update(&ctx, (const uint8_t*)hash.constData(), hash.size());
        uint8_t dgst[32];
        sm3_finish(&ctx, dgst);
        hash = QByteArray((char*)dgst, 32);
    }
    return actualSalt + hash; // [盐值32字节][哈希32字节]
}

注册用户界面

服务器端注册日志输出

3.1.2 用户登录认证

注册成功后,用户可以进入程序主界面,对自己的文件进行管理。

实现思路

  • 用户输入凭据后,客户端与服务器进行SM2密钥协商
  • 建立安全信道后发送登录请求
  • 服务器验证密码哈希
  • 建立用户会话

核心实现代码

cpp 复制代码
// SM2密钥协商 - 客户端
bool SecurityManager::performClientHandshake(QTcpSocket* socket) {
    // 1. 接收服务器SM2公钥
    QByteArray serverPublicKeyBytes = readData(socket, 64);
    // 2. 生成会话密钥
    sessionKey = generateRandomKey(16);
    // 3. SM2加密会话密钥
    QByteArray encryptedSessionKey = encryptWithSM2PublicKey(sessionKey, serverPublicKeyBytes);
    // 4. 发送加密的会话密钥
    socket->write(encryptedSessionKey);
    // 5. 等待握手确认
    QString response = readString(socket);
    return response == "HANDSHAKE_OK";
}

系统登录界面

** SM2密钥协商过程日志**

用户注销后,会返回登录界面,此时用户已从服务器中下线。

3.2 安全文件传输系统

这个模块主要实现文件的安全传输,确保用户数据的安全性。同时,在这里使用了大量的Linux系统调用,尤其是文件调用。

3.2.1 文件上传功能

实现流程

  1. 用户选择文件
  2. 客户端使用SM4加密文件数据
  3. 计算文件SM3哈希值
  4. 分块传输到服务器
  5. 服务器解密并验证完整性

核心实现代码

cpp 复制代码
// SM4文件加密 - FileTransfer类
bool FileTransfer::uploadFile(const QString& filepath) {
    QFile file(filepath);
    if (!file.open(QIODevice::ReadOnly)) return false;
    
    QByteArray fileData = file.readAll();
    
    // SM4加密
    QByteArray encryptedData = securityManager->encryptSM4(fileData);
    
    QJsonObject request;
    request["type"] = "upload";
    request["filename"] = QFileInfo(filepath).fileName();
    request["filesize"] = encryptedData.size();
    request["filehash"] = QString(securityManager->calculateSM3Hash(fileData).toHex());
    
    // 发送请求头
    socket->write(QJsonDocument(request).toJson());
    const int chunkSize = 8192;
    int bytesWritten = 0;
    
    while (bytesWritten < encryptedData.size()) {
        QByteArray chunk = encryptedData.mid(bytesWritten, chunkSize);
        int written = socket->write(chunk);
        bytesWritten += written;
        emit uploadProgress(bytesWritten, encryptedData.size());
    }
    return true;
}

文件管理界面

文件上传成功后,可以点击四个表头,以根据名称、类型、大小或上传时间对文件进行排序。点击一次为升序排序,再次点击则为降序排序。

服务器端文件接收日志

3.2.2 文件下载功能

实现要点

  • 用户权限验证(只能下载自己的文件)
  • SM4解密文件数据
  • 完整性校验

选择下载地址

服务器端发送文件日志

3.3 审计日志系统

3.3.1 日志记录架构

设计思路

  • 单例模式设计,线程安全
  • 多级别日志:DEBUG、INFO、WARNING、ERROR、CRITICAL
  • 多分类日志:SYSTEM、AUTH、CRYPTO、FILE_OP、DATABASE、NETWORK、SECURITY
  • 按用户和日期分离存储

核心实现代码

cpp 复制代码
// 审计日志记录 - AuditLogger类
class AuditLogger {
public:
    enum LogLevel { DEBUG, INFO, WARNING, ERROR, CRITICAL };
    enum LogCategory { SYSTEM, AUTH, CRYPTO, FILE_OP, DATABASE, NETWORK, SECURITY };
    static AuditLogger& getInstance() {
        static AuditLogger instance;
        return instance;
    }
    void log(LogLevel level, LogCategory category, const QString& username, 
             const QString& action, const QString& resource, bool success, 
             const QString& details = "") {
        QMutexLocker locker(&logMutex);
        QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        QString logEntry = QString("[%1] [%2] [%3] %4: %5 on '%6' - %7 (%8)")
                          .arg(timestamp)
                          .arg(levelToString(level))
                          .arg(categoryToString(category))
                          .arg(username)
                          .arg(action)
                          .arg(resource)
                          .arg(success ? "SUCCESS" : "FAILED")
                          .arg(details);
        
        // 写入系统日志
        writeToSystemLog(logEntry);
        // 写入用户个人日志
        if (!username.isEmpty() && username != "SYSTEM") {
            writeToUserLog(username, logEntry);
        }
    }
};

3.3.2 用户日志下载

实现功能

  • 客户端可以下载自己的操作日志
  • 支持日期范围筛选
  • SM4加密传输确保安全

** 日志下载界面**

** 用户日志内容**

** 日志下载事件**

以下是你提供内容的润色和扩展版,保持结构清晰、语言规范,并适度增加技术细节和逻辑说明,适合用于实验报告:


4、其他功能

4.1 Socket编程

本系统采用基于 Qt 网络模块的 Socket 编程实现了客户端与服务器之间的可靠通信。

服务器端:

服务器端通过继承 QTcpServer 类,实现了基于 TCP 协议的监听与连接管理功能。当检测到新的客户端连接请求时,系统会自动创建一个独立的 ClientHandler 线程对象,专门负责处理该客户端的所有通信任务。此种"主线程监听 + 子线程处理"的结构设计,能够有效支持多客户端并发访问,显著提升了系统的并发处理能力与整体稳定性。

客户端:

客户端采用 QTcpSocket 实现与服务器的连接功能。通信模块被封装为 FileTransfer 类,统一管理网络连接的建立、文件数据的分片发送与接收、错误处理与状态监控等逻辑。该模块的封装提高了通信代码的可复用性与可维护性,使得客户端逻辑更加简洁明晰,便于后期拓展和维护。

服务器端关键实现:

cpp 复制代码
class ServerCore : public QTcpServer {
    Q_OBJECT
    
public:
    void start(quint16 port) {
        if (this->listen(QHostAddress::Any, port)) {
            qDebug() << "服务器启动,监听端口:" << port;
        } else {
            qDebug() << "服务器启动失败:" << this->errorString();
        }
    }
    
protected:
    // 重写虚函数,处理新连接
    void incomingConnection(qintptr socketDescriptor) override {
        ClientHandler *handler = new ClientHandler(socketDescriptor, this);
        connect(handler, &QThread::finished, handler, &QObject::deleteLater);
        handler->start();  // 启动新线程处理客户端
    }
};

该设计模式通过将每个客户端请求交由独立线程处理,有效避免了主线程阻塞的问题,同时保障了系统在高并发情况下的稳定运行。


4.2 Linux系统调用支持

系统在文件操作模块中大量使用了基于 Qt 封装的 Linux 系统调用,确保文件传输的高效性与兼容性。文件的创建、写入、读取等底层操作均对应于实际的系统调用,如 openwritereadfsyncstat 等。

文件管理模块关键实现:

cpp 复制代码
class FileManager {
public:
    // 文件创建和写入
    static bool saveFile(const QString& filepath, const QByteArray& data) {
        QFile file(filepath);
        QDir dir = QFileInfo(filepath).dir();
        if (!dir.exists()) {
            dir.mkpath(".");  // 等效于 mkdir
        }
        if (!file.open(QIODevice::WriteOnly)) {
            qDebug() << "文件创建失败:" << file.errorString();
            return false;
        }
        qint64 bytesWritten = file.write(data);  // 等效于 write
        if (bytesWritten != data.size()) {
            qDebug() << "文件写入不完整";
            return false;
        }
        file.flush();  // 等效于 fsync
        return true;
    }
    // 文件读取
    static QByteArray loadFile(const QString& filepath) {
        QFile file(filepath);
        if (!file.exists()) {  // 等效于 stat
            qDebug() << "文件不存在:" << filepath;
            return QByteArray();
        }
        if (!file.open(QIODevice::ReadOnly)) {
            qDebug() << "文件打开失败:" << file.errorString();
            return QByteArray();
        }
        return file.readAll();  // 等效于 read
    }
};

4.3 身份鉴别及访问控制

系统在身份认证和访问控制方面引入了基于角色的权限管理机制。用户在登录时必须经过身份鉴别,认证成功后,系统会根据其账号信息为其分配对应的权限范围。普通用户仅能访问自己上传的文件,无法访问其他用户的数据,从而有效保障了数据的隔离性和安全性。

此外,为增强身份认证的安全性,系统采用口令加密存储机制,使用 SM3 哈希算法对用户口令进行摘要处理,并加入随机盐值(salt)进行混淆,以防止彩虹表攻击和哈希碰撞带来的威胁。认证流程在前后端之间通过安全通道进行,确保用户凭据在传输过程中不会被窃取或篡改。

这种精细化的访问控制策略不仅提升了系统的安全等级,也符合多用户环境下的文件管理需求。


二、安全性与密码完成以及完善过程

1、安全性设计与密码完成

1.1 SM3密码哈希存储机制

本系统在用户身份认证中采用SM3哈希与加盐机制提升安全性。用户注册时,系统生成32位随机盐值并与口令组合,使用SM3进行初始化哈希后执行1000轮迭代加密,最终将盐值与哈希结果组合存入数据库。登录时,系统提取存储的盐值和哈希,使用用户输入的口令重新执行相同的加密流程,最后将计算结果与数据库中的哈希值进行比对,以验证身份是否匹配,确保口令安全存储与验证的完整性。

SM3加密算法实现

csharp 复制代码
QByteArray hashPasswordWithSalt(const QString& password, const QByteArray& salt = QByteArray()) {
    QByteArray actualSalt = salt.isEmpty() ? generateSalt() : salt;
    QByteArray combined = password.toUtf8() + actualSalt;
    // 多轮哈希 (PBKDF2 密钥拉伸)
    QByteArray hash = combined;
    const int rounds = 10000;  // 10000轮SM3哈希
    SM3_CTX ctx;
    uint8_t dgst[32];
    for (int i = 0; i < rounds; i++) {
        sm3_init(&ctx);
        sm3_update(&ctx, (const uint8_t*)hash.constData(), hash.size());
        sm3_finish(&ctx, dgst);
        hash = QByteArray((char*)dgst, 32);
    }
    // 返回 [32字节盐值] + [32字节哈希值]
    return actualSalt + hash;
}

1.2 混合密码系统实现

混合密码系统的实现流程从客户端启动开始。客户端首先生成一对SM2密钥(公钥和私钥),用于后续的密钥交换与加密通信。服务器端在启动时加载预先配置好的SM2密钥对,准备与客户端进行密钥协商。在连接建立后,服务器向客户端发送其SM2公钥,客户端接收后,结合自身私钥生成对称加密用的SM4会话密钥,并使用服务器公钥对该密钥加密后发送回服务器。

服务器接收到加密后的会话密钥后,使用自身的SM2私钥进行解密,从而获取SM4密钥,双方完成共享。为确保通信安全,系统在握手阶段进行确认,确保SM4密钥协商成功并一致,建立安全通信通道。每次通信会话系统都会生成独立的SM4密钥,并定期更新,降低长期使用带来的安全风险。

在数据传输过程中,客户端与服务器使用协商的SM4密钥进行加解密操作,确保数据传输的机密性与完整性。该混合密码机制结合SM2的密钥交换优势与SM4的高效对称加密能力,有效保障了通信过程中的数据安全。

1.2.1 SM2椭圆曲线密钥协商

  • 客户端密钥生成
csharp 复制代码
void SecurityManager::generateSM2KeyPair() {
    if (sm2_key_generate(&clientSM2Key) != 1) {
        qDebug() << "SM2密钥对生成失败!";
        return;
    }
    
    // 提取64字节公钥 (x,y坐标各32字节)
    uint8_t public_key[64];
    if (sm2_z256_point_to_bytes(&clientSM2Key.public_key, public_key) == 1) {
        clientPublicKey = QByteArray((char*)public_key, 64);
    }
}
  • 服务器端密钥协商流程
csharp 复制代码
bool performCryptoHandshake(QTcpSocket& socket) {
    // 1. 发送服务器SM2公钥
    if (!sendServerPublicKey(socket)) return false;
    
    // 2. 接收客户端加密的SM4会话密钥
    if (!receiveEncryptedSessionKey(socket)) return false;
    
    // 3. 发送握手确认
    handshakeCompleted = true;
    return true;
}

1.2.2 SM4对称加密系统

  • 会话密钥生成
csharp 复制代码
void generateSessionKey() {
    // 生成128位(16字节)随机会话密钥
    sessionKey.resize(16);
    auto generator = QRandomGenerator::global();
    for (int i = 0; i < 16; i++) {
        sessionKey[i] = generator->bounded(256);
    }
}
  • 会话密钥生成

1.2.3 SM3完整性校验

  • 文件哈希计算
csharp 复制代码
QString calculateHash(const QByteArray& data) {
    SM3_CTX ctx;
    uint8_t dgst[32];
    
    sm3_init(&ctx);
    sm3_update(&ctx, (const uint8_t*)data.constData(), data.size());
    sm3_finish(&ctx, dgst);
    
    return QByteArray((char*)dgst, 32).toHex();
}

1.3 SM4-CTR加密和SM3传输文件

本系统采用国密算法实现了文件在传输与存储过程中的双重安全保障。客户端首先利用 SM4 加密算法的计数器模式(CTR) 对文件内容进行加密。该模式具有良好的并行性和随机性,确保即使相同的明文也能生成不同的密文,从而有效防止内容泄露与模式分析。同时,客户端对原始文件数据计算其 SM3 摘要值,作为数据完整性的校验依据。

加密后的数据、生成的哈希值以及相关文件信息一并发送至服务器。服务器端接收到数据后,先用相同的会话密钥和初始化向量(IV)进行 SM4-CTR 解密,还原出原始数据内容。随后服务器重新计算一次该数据的 SM3 哈希值,并与客户端发送的哈希值进行比对。若两者一致,说明文件内容在传输过程中未被篡改,服务器则允许存入数据库;若不一致,则拒绝保存并报告完整性校验失败。

该机制实现了文件加密传输与可信存储的全流程防护,不仅保障了用户数据的机密性,也有效防止了数据被非法篡改或伪造的风险,特别适用于对数据安全要求较高的业务场景。


2、完善过程

问题1:SM2公钥生成全为零

问题现象

服务器生成的公钥全为零,即:

复制代码
服务器公钥: 0000000000000000...(64字节全为0)

原因分析

错误的公钥提取方式,直接使用内存拷贝结构体,导致公钥数据不正确。

解决方案

修复前(错误)的代码如下:

cpp 复制代码
memcpy(publicKey, &serverSM2Key.public_key, 64);

修复后(正确)的代码如下:

cpp 复制代码
uint8_t public_key[64];
if (sm2_z256_point_to_bytes(&serverSM2Key.public_key, public_key) == 1) {
    serverPublicKey = QByteArray((char*)public_key, 64);
}

通过使用sm2_z256_point_to_bytes函数正确提取公钥数据,避免直接内存拷贝结构体的问题。

验证结果

修复后,服务器公钥正确生成,示例如下:

复制代码
服务器公钥: "14a527b1d209d437854e67007648abbaddc00118df6c52191185a2ed0ccb85fd..."
64字节公钥正确生成

问题2 SM3密码杂凑算法

问题现象

常规的加密安全程度不够

解决方案

cpp 复制代码
// 安全的密码存储方案
1. 生成32字节随机盐值
2. 密码+盐值组合
3. 进行10000轮SM3哈希运算
4. 存储格式:[盐值32字节][哈希32字节]

验证结果

复制代码
输入测试密码: "707"
盐值长度: 32 内容: "0960b09cd073e238d6004605a217a8f6..."
开始 10000 轮哈希计算
最终结果长度: 64 内容: "0960b09cd073e238...9e8244c4dcd158494..."
10000轮哈希计算成功

问题3:SM2公钥生成全为零

问题现象

客户端在注册过程中出现无限等待服务器响应的情况。

原因分析

由于服务器错误地将注册请求当作登录请求处理,导致注册流程也尝试执行密钥协商。而注册过程中并不需要密钥协商,结果造成客户端在等待服务器发送SM2公钥的过程中陷入阻塞。

解决方案实现

cpp 复制代码
// 在服务器端增加请求类型检测
void ServerCore::handleClientConnection(QTcpSocket* socket) {
    QString requestStr = readString(socket);
    QJsonObject obj = QJsonDocument::fromJson(requestStr.toUtf8()).object();
    QString requestType = obj["type"].toString();
    bool needsHandshake = true;
    if (requestType == "register") {
        needsHandshake = false; // 注册跳过密钥协商
        handleRegister(socket, obj);
        return;
    }
    if (needsHandshake) {
        if (!performServerHandshake(socket)) {
            socket->close();
            return;
        }
    }
    if (requestType == "login") {
        handleLogin(socket, obj);
    }
}

通过在服务器端添加对请求类型的判断,明确区分注册与登录请求,仅在登录请求中执行密钥协商,避免了不必要的SM2操作。

验证结果

复制代码
检测到请求类型: "register"
检测到注册请求,跳过密钥协商,直接处理注册
注册流程正常
登录密钥协商正常

问题4:多线程数据库连接冲突

问题现象

在多客户端并发访问的场景下,服务器端的数据库操作偶尔失败,表现为无法打开数据库或数据读写异常。

原因分析

Qt 中的 QSqlDatabase 对象在多线程环境下不具备线程安全性,若多个线程共用一个数据库连接,会导致连接冲突或资源竞争,从而导致数据库操作失败。

解决方案实现

cpp 复制代码
// 为每个线程创建独立的数据库连接
QString dbName = "server_thread_" + QString::number((quintptr)QThread::currentThreadId());
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", dbName);
db.setDatabaseName("server.db");

if (!db.open()) {
    qDebug() << "线程" << QThread::currentThreadId() << "数据库连接失败";
    return false;
}

通过为每个线程指定唯一的连接名,避免不同线程共享同一数据库连接,有效解决并发冲突问题。

验证结果

复制代码
线程ID: 0x16c 打开数据库成功
线程ID: 0x1a0 打开数据库成功
多线程并发访问数据库正常,未再出现连接失败或写入异常

三、项目改动和未来打算

在完成基础系统功能开发后,我认真评估了现有成果的优缺点,并结合实际开发体验与系统运行情况,未来可能从以下方面对系统进行完善:

1. 文件管理功能完善

  • 文件重命名:支持用户修改文件名,避免重名冲突并更新数据库索引。
  • 文件夹与目录结构支持:引入"文件夹"概念,实现基本层级目录管理,使用户在面对大量文件时能更好地分类和管理。

2. 搜索与筛选功能

  • 模糊搜索:支持文件名模糊匹配查询;
  • 组合查询:可支持同时输入多个关键词、按多个维度联合筛选。

3. 批量操作与交互优化

  • 多文件批量上传与下载:通过文件选择器支持多选,并在传输队列中统一管理;
  • 批量删除与分享:支持选中多个文件同时进行删除、生成分享链接等操作;
  • 文件上传/下载进度条优化:提供总进度、剩余时间等信息提示,改善用户等待体验。

4. 文件加密与验证机制强化

  • 当前系统采用SM4加密和SM3哈希作为基础安全手段;
  • 后续可尝试引入"数字签名 + 时间戳"机制,防止篡改与伪造;
  • 若部署于多用户系统,可考虑对每个用户使用独立密钥进行文件加密。

相关推荐
老纪的技术唠嗑局8 分钟前
重剑无锋,大巧不工 —— OceanBase 中的 Nest Loop Join 使用技巧分享
数据库·sql
未来之窗软件服务37 分钟前
JAVASCRIPT 前端数据库-V6--仙盟数据库架构-—-—仙盟创梦IDE
数据库·数据库架构·仙盟创梦ide·东方仙盟·东方仙盟数据库
一只爱撸猫的程序猿2 小时前
构建一个简单的智能文档问答系统实例
数据库·spring boot·aigc
nanzhuhe2 小时前
sql中group by使用场景
数据库·sql·数据挖掘
消失在人海中2 小时前
oracle sql 语句 优化方法
数据库·sql·oracle
Clang's Blog2 小时前
一键搭建 WordPress + MySQL + phpMyAdmin 环境(支持 PHP 版本选择 & 自定义配置)
数据库·mysql·php·wordpr
zzc9212 小时前
MATLAB仿真生成无线通信网络拓扑推理数据集
开发语言·网络·数据库·人工智能·python·深度学习·matlab
未来之窗软件服务3 小时前
JAVASCRIPT 前端数据库-V1--仙盟数据库架构-—-—仙盟创梦IDE
数据库·数据库架构·仙盟创梦ide·东方仙盟数据库
LjQ20403 小时前
网络爬虫一课一得
开发语言·数据库·python·网络爬虫
Guheyunyi3 小时前
监测预警系统重塑隧道安全新范式
大数据·运维·人工智能·科技·安全