【基于Qt6.5和H5做一个简单的物料管理ERP系统的架构设计】

前言

现在准备自己做一个简单的物料管理系统,虽然小,但以小见大,基本可以搞懂前端后端设设计开发流程,本篇就先说一下系统的设计架构。

先看看效果:

开发环境:Qt 6.5 + SQLite + H5(前后端分离)+ Ubuntu 24.04 ;

核心特性: 三级用户权限、密码重置 / 修改(旧密码验证)、指定目录数据库检查、Qt6.5/Ubuntu24.04 适配;

设计目标: 轻量化部署、权限隔离、密码安全强化、跨 Qt 版本兼容、Ubuntu 环境稳定运行 ;

一、设计概述

1.1 系统定位

本系统为小型企业级物料管理 ERP 系统,采用前后端分离架构,后端基于 Qt 6.5(Ubuntu24.04 环境)实现轻量级 HTTP 服务,前端基于 H5 实现可视化交互,嵌入式 SQLite 数据库存储数据。

1.2 核心技术栈(Qt6.5+Ubuntu24.04 )

分层 技术选型 核心组件 / 工具
后端 C++17、Qt 6.5(Ubuntu24.04) QTcpServer/QTcpSocket(Qt6 Network 模块)、Qt Sql、QCryptographicHash(Qt6 加密)、QDir(目录操作)、Qt6 Pro 模块声明
前端 H5TML/CSS/JavaScript Fetch API、MD5.js、原生 DOM 操作
数据层 SQLite 指定目录存储,启动自动检查 / 创建、Ubuntu 权限适配
通信协议 HTTP/1.1 JSON 格式数据交互、Token 鉴权
编译 / 运行环境 Ubuntu 24.04 LTS GCC 13.2、Qt 6.5 SDK、libsqlite3-dev

1.3 核心功能

  • 权限隔离:超级管理员 / 管理员 / 一般用户三级权限体系;
  • 密码安全:修改密码强制验证旧密码,MD5 加密规则跨 Qt 版本统一;
  • 数据管理:分为用户数据表和物料数据表,专门针对用户管理和物料管理;
  • 数据库规范化:Ubuntu 环境下安全创建指定目录,自动检查 / 连接数据库;
  • 轻量化部署:Ubuntu 系统下无外部服务依赖,Qt6 后端直接提供 HTTP 服务。

二、整体目录结构(代码)

typescript 复制代码
erp-material-system/  # 根目录
├── backend/                # Qt 后端(控制台工程)
│   ├── CMakeLists.txt      # Qt6工程配置文件
│   ├── main.cpp            # 程序入口(Qt 初始化、数据库检查、HTTP服务启动)
│   ├── httpserver.h/.cpp   # 接入层:Qt  Network适配的HTTP服务核心
│   ├── connectionhandler.h/.cpp   # 路由处理:Qt  对接入层的连接进行路由分类
│   ├── authutil.h/.cpp     # 权限层:Token管理、Qt MD5加密、权限校验
│   ├── dbutil.h/.cpp       # 数据层:Ubuntu目录权限适配、数据库检查/创建
│   ├── usercontroller.h/.cpp # 业务层:用户管理,密码修改,密码重置,Qt6 SQL操作
│   └── materialcontroller.h/.cpp # 业务层:物料管理、Qt6 SQL操作
├── frontend/               # 前端H5(跨平台,无需适配)
│   ├── js/
│       ├── login.html          # 登录页
│       ├── index.html          # 主页面
│       ├── user-manage.html    # 用户管理页
│       ├── material-manage.html # 物料管理页
│       ├── material-search.html # 物料查询页
│   ├── css/style.css       # 全局样式
│   └── js/
│       ├── md5.js          # MD5加密工具
│       ├── api.js          # 接口请求封装
│       └── util.js         # 通用工具
└── data/                   # 数据库目录(Ubuntu下需确保读写权限)
    └── erp_material.db     # SQLite数据库文件

实际如下:

三、系统架构设计

3.1 架构总览(分层 + 交互)

typescript 复制代码
┌─────────────────┐      HTTP/1.1 + JSON      ┌─────────────────────────┐
│ 前端H5层        │─────────────────────────▶│ 后端Qt  HTTP服务     │
│ - 表现层        │◀─────────────────────────│ - 接入层(Qt6 Network) │
│ - 交互层        │         Token鉴权         │ - 权限认证层            │
│ - 网络请求层    │                           │ - 业务逻辑层            │
└─────────────────┘                           └────────┬────────────────┘
                                                       │
                                                       ▼
                                               ┌─────────────────────────┐
                                               │ 数据层(SQLite)|
                                               │ - Ubuntu指定目录检查    │
                                               │ - 自动创建/连接(权限适配)│
                                               └─────────────────────────┘

3.2 分层详细设计

3.2.1 前端分层设计
分层 职责描述 对应文件 / 目录
表现层 页面布局、样式展示、权限相关 UI 控制(如修改密码强制填写旧密码) *.html、css/style.css
交互层 表单校验、弹窗交互、菜单路由、权限判断 js/util.js、各页面内联 JS
网络请求层 统一接口请求封装、Token 携带、响应状态码处理 js/api.js、js/md5.js
3.2.2 后端分层设计
分层 职责描述( Qt6/Ubuntu ) 对应文件
接入层 1. Qt6 QTcpServer/QTcpSocket 监听端口;2. 解析 HTTP 请求;3. Ubuntu 跨域处理;4. 路由各个接口 httpserver.h/.cpp、connectionhandler.h/.cpp
权限认证层 1. Token 生成 / 校验;2. Qt6 QCryptographicHash 加密;3. 三级权限判断 authutil.h/.cpp
业务逻辑层 1. 密码修改旧密码验证;2. 物料出入库逻辑;3. SQL 操作 usercontroller.h/.cpp、materialcontroller.h/.cpp
数据访问层 1. Ubuntu24.04 下data目录权限检查;2. Qt6 QDir/QFile 创建目录 / 文件;3. Qt6 QSqlDatabase 连接 SQLite;4. 防 SQL 注入 dbutil.h/.cpp(核心适配)

后端以网络请求传输流程来分的话,具体主要包括以下几个核心层次:
网络层 :

  • 基于QTcpServer和QTcpSocket实现的自定义HTTP服务器
  • 支持多线程并发处理请求
  • 实现了完整的HTTP协议解析和响应构建

路由层 :

  • 静态路由注册机制,将HTTP方法和路径映射到处理函数
  • 支持RESTful API设计风格
  • 提供统一的请求分发和错误处理

业务逻辑层 :

  • 控制器模式设计,如UserController、MaterialController
  • 实现了用户认证、物料管理等核心业务逻辑
  • 与数据访问层解耦,便于维护和扩展

数据访问层 :

  • 基于SQLite的数据库操作封装
  • 提供统一的查询和执行接口
  • 支持事务处理和错误恢复

工具层 :

  • 认证工具(AuthUtil):处理Token生成和验证
  • HTTP工具(httputil.h):定义请求和响应数据结构
  • 日志工具:基于Log4Qt实现的日志记录
3.2.3 数据层设计(SQLite+Ubuntu 权限适配)

(1)数据库目录与权限规范

配置项 取值 说明(Ubuntu 适配)
数据库文件名 erp_material.db 固定名称
目录创建权限 0755 Ubuntu 下创建目录时设置可读可写权限
文件权限 0644 SQLite 文件确保进程可读写
权限检查 QFile::permissions() 启动时检查目录 / 文件权限,不足则自动修正

(2)核心数据表结构(无变化)

① 用户表(t_user)

字段名 类型 约束 说明
id INTEGER PRIMARY KEY 自增主键
username VARCHAR(50) NOT NULL UNIQUE 登录用户名(唯一)
password VARCHAR(32) NOT NULL MD5 加密密码(盐值:erp_2025)
role TINYINT NOT NULL 角色(0 = 一般用户,1 = 管理员,2 = 超级管理员)
create_time DATETIME NOT NULL 用户创建时间
reset_time DATETIME - 最近密码重置时间

② 物料表(t_material)

字段名 类型 约束 说明
id INTEGER PRIMARY KEY 自增主键
name VARCHAR(100) NOT NULL 物料名称
supplier VARCHAR(100) NOT NULL 供应商名称
stock_num INTEGER NOT NULL 库存数量
supplier_tel VARCHAR(20) NOT NULL 供应商联系电话
in_time DATETIME 最近入库时间
out_time DATETIME 最近出库时间
operator_id INTEGER 操作人 ID(关联 t_user.id)
update_time DATETIME NOT NULL 最后更新时间

四、权限体系设计(无核心变化)

4.1 三级角色定义

角色 标识(role) 适用人群 核心定位
超级管理员 2 开发 / 运维人员 系统最高权限,维护全量用户 / 密码
管理员 1 企业业务管理员 管理一般用户、全量物料操作
一般用户 0 普通员工 仅物料查询、修改自身密码

4.2 权限矩阵(用户分层管理)

操作类型 超级管理员(2) 管理员(1) 一般用户(0) 备注
密码操作 - 修改自身 支持(强制验证旧密码) 支持(强制验证旧密码) 支持(强制验证旧密码) 后端加密逻辑与前端保持一致
物料管理 - 增删改 支持 支持
其他操作-查看 支持 支持 支持

4.3 Token 鉴权机制

  • 生成:Qt 使用QUuid::createUuid().toString()生成 Token;
  • 存储: QMap<QString, QPair<int, int>> 存储 Token 与用户 ID / 角色映射;
  • 校验:所有需权限接口携带 Token,Qt 解析请求头逻辑兼容 Ubuntu 字符编码。

五、核心模块设计

5.1 用户管理模块

5.1.1 核心功能( API 适配)
功能点 触发场景 Qt 适配说明
修改自身密码 所有用户操作 QCryptographicHash 加密,确保前后端加密结果统一
用户登录 前端登录页提交 QSqlQuery 执行 SQL 兼容 Ubuntu SQLite 驱动
5.1.2 核心代码片段(密码加密)
cpp 复制代码
// authutil.cpp -> Qt6.5 MD5加密实现(Ubuntu24.04)
#include <QCryptographicHash>
#include <QString>

QString AuthUtil::encryptPassword(const QString& password) {
    // MD5加密(与前端md5.js结果一致)m_salt="erp_2025"
    QString passwordWithSalt = password + m_salt;
    QByteArray data = passwordWithSalt.toUtf8();
    QByteArray hashBytes = QCryptographicHash::hash(data, QCryptographicHash::Md5);
    QByteArray lowerHexBytes = hashBytes.toHex().toLower();
    return QString(lowerHexBytes);
}

5.2 数据层模块

5.2.1 核心功能
功能点 触发时机 核心逻辑
目录检查 后端启动时 1. 检查数据库文件是否存在,不存在则创建数据库,创建表,确保数据库存在;2. 存在则连接数据库
数据库连接 后端启动时 1. QSqlDatabase 连接 SQLite;2. 确保当前用户有data目录读写权限
5.2.2 核心代码片段(数据库初始化)
cpp 复制代码
bool DbUtil::initDb()
{
    safeLock(); // 手动安全加锁
    bool isInitSuccess = false;

    // ========== 步骤1:检查并创建数据库目录 ==========
    QDir dbDir(m_dbDirPath);
    if (!dbDir.exists()) {
        qInfo() << "[INFO] 数据库目录不存在,开始创建:" << m_dbDirPath;
        if (!dbDir.mkpath(".")) {
            qCritical() << "[ERROR] 数据库目录创建失败:" << m_dbDirPath;
            safeUnlock(); // 解锁后返回
            return false;
        }
        // 设置目录权限(带超时,避免阻塞)
        QProcess chmodProcess;
        chmodProcess.start("chmod", QStringList() << "755" << m_dbDirPath);
        if (!chmodProcess.waitForFinished(5000)) {
            qWarning() << "[WARNING] 数据库目录权限设置超时,权限可能不足";
        }
        qInfo() << "[INFO] 数据库目录创建成功:" << m_dbDirPath;
    }

    // ========== 步骤2:检查数据库文件,分支处理 ==========
    QFileInfo dbFileInfo(m_dbFilePath);
    bool isDbFileExist = dbFileInfo.exists() && dbFileInfo.isFile();

    // 分支A:无数据库文件 → 先新建空文件 → 连接 → 建表 → 初始化
    if (!isDbFileExist) {
        qInfo() << "[INFO] 未检测到数据库文件,开始新建:" << m_dbFilePath;

        // 2.1 显式创建空数据库文件
        if (!createEmptyDbFile()) {
            qCritical() << "[ERROR] 空数据库文件创建失败,初始化终止";
            safeUnlock(); // 解锁后返回
            return false;
        }

        // 2.2 连接新建的空数据库
        QSqlDatabase newDb = connectDb("main_conn_new");
        if (!newDb.isOpen()) {
            qCritical() << "[ERROR] 新建数据库连接失败,初始化终止";
            safeUnlock(); // 解锁后返回
            return false;
        }
        qInfo() << "[INFO] 新建数据库连接成功";

        // 2.3 建表(仅新建数据库才会进来)
        if (!createAllTables()) {
            qCritical() << "[ERROR] 新建数据库-表创建失败";
            newDb.close();
            QSqlDatabase::removeDatabase("main_conn_new");
            safeUnlock(); // 解锁后返回
            return false;
        }

        // 2.4 初始化默认管理员(仅新建数据库时添加超级管理员)
        if (!initDefaultAdmin()) {
            qWarning() << "[WARNING] 新建数据库-默认管理员创建失败(可手动补充)";
        }

        // 2.5 关闭临时连接,初始化成功
        newDb.close();
        QSqlDatabase::removeDatabase("main_conn_new");
        isInitSuccess = true;
        qInfo() << "[INFO] 新建数据库初始化完成:文件创建+表初始化+管理员配置";
    }
    else
    {
        // 分支B:有数据库文件 → 直接连接
        qInfo() << "[INFO] 检测到已有数据库文件,开始直接连接:" << m_dbFilePath;

        QSqlDatabase testDb = QSqlDatabase::addDatabase("QSQLITE", "init_test_conn");
        testDb.setDatabaseName(m_dbFilePath);

        if (!testDb.open()) {
            qCritical() << "[ERROR] 数据库连接失败:" << testDb.lastError().text();
            safeUnlock();
            return false;
        }

        // 执行简单查询测试连接
        QSqlQuery testQuery("SELECT 1", testDb);
        if (!testQuery.exec()) {
            qCritical() << "[ERROR] 数据库连接测试失败:" << testQuery.lastError().text();
            testDb.close();
            safeUnlock();
            return false;
        }

        testDb.close();

        // 移除测试连接
        QSqlDatabase::removeDatabase("init_test_conn");

        isInitSuccess = true;
        qInfo() << "[INFO] 已有数据库连接成功,无需初始化表/管理员";
    }

    safeUnlock(); // 最终解锁
    return isInitSuccess;
}

5.3 接入层模块

5.3.1 启动服务
cpp 复制代码
// httpserver.cpp
HttpServer::HttpServer(QObject *parent)
    : QTcpServer(parent)
{
    qInfo() << QString("[%1] [INFO] HttpServer构造函数开始执行(主线程ID:%2)")
                   .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
                   .arg((quintptr)QThread::currentThreadId(), 0, 16);

    // 安全初始化线程池(双重检查+内存泄漏防护)
    if (m_threadPool == nullptr) {
        m_threadPool = new QThreadPool(this);
        if (m_threadPool == nullptr) {
            qCritical() << QString("[%1] [ERROR] QThreadPool创建失败!").arg(QDateTime::currentDateTime().toString());
            return;
        }
        // 默认线程池配置
        m_threadPool->setMaxThreadCount(10);
        m_threadPool->setExpiryTimeout(30000);
        m_threadPool->setStackSize(1024 * 1024); // 线程栈大小1MB(防栈溢出)
    }
    
    // 添加定期清理过期Token的定时器(每小时执行一次)
    m_cleanupTimer = new QTimer(this);
    connect(m_cleanupTimer, &QTimer::timeout, this, &HttpServer::cleanupExpiredTokens);
    m_cleanupTimer->start(3600000); // 3600000毫秒 = 1小时

    qInfo() << QString("[%1] [INFO] HttpServer构造函数执行完成,线程池初始化成功(最大并发:%2)")
                   .arg(QDateTime::currentDateTime().toString())
                   .arg(m_threadPool->maxThreadCount());
}

// 启动HTTP服务
bool HttpServer::start(quint16 port, int threadPoolSize, qint64 maxRequestSize, int connectionTimeout) {
    qInfo() << QString("[%1] [INFO] 进入HttpServer::start,端口:%2,线程池大小:%3,最大请求大小:%4KB,连接超时:%5s")
                   .arg(QDateTime::currentDateTime().toString())
                   .arg(port)
                   .arg(threadPoolSize)
                   .arg(maxRequestSize / 1024)
                   .arg(connectionTimeout / 1000);

    // 检查线程池初始化
    if (m_threadPool == nullptr) {
        qCritical() << QString("[%1] [ERROR] 线程池未初始化,启动失败").arg(QDateTime::currentDateTime().toString());
        return false;
    }

    // 更新配置
    m_threadPool->setMaxThreadCount(threadPoolSize);
    m_maxRequestSize = maxRequestSize;
    m_connectionTimeout = connectionTimeout;

    // 监听所有IP
    if (!this->listen(QHostAddress::AnyIPv4, port)) { // 先IPv4,避免IPv6兼容问题
        qCritical() << QString("[%1] [ERROR] HTTP服务启动失败:%2,端口:%3")
                           .arg(QDateTime::currentDateTime().toString())
                           .arg(this->errorString())
                           .arg(port);
        return false;
    }
    
    // 立即执行一次Token清理,确保服务器启动时清理过期Token
    cleanupExpiredTokens();
    qInfo() << QString("[%1] [INFO] HTTP服务启动成功")
                       .arg(QDateTime::currentDateTime().toString())
                   + QString("\n  - 监听地址:%1:%2")
                         .arg(this->serverAddress().toString())
                         .arg(this->serverPort())
                   + QString("\n  - 最大并发数:%1").arg(m_threadPool->maxThreadCount())
                   + QString("\n  - 最大请求大小:%1KB").arg(m_maxRequestSize / 1024)
                   + QString("\n  - 连接超时:%1s").arg(m_connectionTimeout / 1000)
                   + QString("\n  - 跨域启用:%1").arg(m_enableCors ? "是" : "否");

    return true;
}
5.3.2 请求解析
cpp 复制代码
HttpRequest ConnectionHandler::parseRequest(const QByteArray& data)
{
    HttpRequest request;
    QString rawData = QString::fromUtf8(data);
    QStringList lines = rawData.split("\r\n");
    if (lines.isEmpty()) return request;

    // 解析首行(方法 + 路径 + 协议)
    QString firstLine = lines[0].trimmed();
    QRegularExpression firstLineRegex(R"(^(\w+)\s+(\S+)\s+HTTP/[\d.]+$)");
    QRegularExpressionMatch firstLineMatch = firstLineRegex.match(firstLine);
    if (firstLineMatch.hasMatch()) {
        request.method = firstLineMatch.captured(1).toUpper();
        QString fullPath = firstLineMatch.captured(2);

        // 解析路径和查询参数
        int queryIndex = fullPath.indexOf('?');
        if (queryIndex != -1) {
            request.path = fullPath.left(queryIndex);
            QString query = fullPath.mid(queryIndex + 1);
            request.params = parseQueryParams(query); // 解析查询参数到 params
        } else {
            request.path = fullPath;
        }
        
        request.path = request.path.trimmed();
        if (!request.path.startsWith("/")) request.path.prepend("/");
        request.path = request.path.replace(QRegularExpression("//+"), "/");
    }

    // 解析请求头
    int bodyStartIndex = -1;
    for (int i = 1; i < lines.size(); ++i) {
        if (lines[i].isEmpty()) {
            bodyStartIndex = i + 1;
            break;
        }
        int colonIndex = lines[i].indexOf(':');
        if (colonIndex != -1) {
            QString key = lines[i].left(colonIndex).trimmed();
            QString value = lines[i].mid(colonIndex + 1).trimmed();
            request.headers[key] = value;
        }
    }

    // 提取Token(优先从Header取)
    request.token = request.headers.value("Authorization", "").replace("Bearer ", "");
    if (request.token.isEmpty()) {
        request.token = request.params["token"].toString();
    }

    // 解析请求体(POST/PUT等方法的JSON参数,合并到params)
    if (bodyStartIndex != -1 && bodyStartIndex < lines.size()) {
        QByteArray bodyData = rawData.mid(rawData.indexOf("\r\n\r\n") + 4).toUtf8();
        request.body = bodyData;
        QJsonObject bodyParams = parseJsonBody(bodyData);

        // 合并参数
        for (auto it = bodyParams.begin(); it != bodyParams.end(); ++it) {
            request.params[it.key()] = it.value();
        }
    }
    request.isValid = true;
    return request;
}
5.3.3 网络请求流程解析

以登录接口(POST /api/login)调用流程为例,完整的调用流程如下:

  1. 前端发起请求 :
  2. HttpServer接收连接 :
    • HttpServer::incomingConnection() 方法接收到新的TCP连接;
    • 创建 ConnectionHandler 实例并提交到线程池执行;
  3. ConnectionHandler处理请求 :
    • ConnectionHandler::run() 方法初始化Socket和事件循环;
    • ConnectionHandler::onReadyRead() 方法读取请求数据;
    • ConnectionHandler::parseRequest() 方法解析HTTP请求,提取方法、路径、请求体等信息;
    • ConnectionHandler::dispatchRoute() 方法根据请求方法和路径查找对应的路由处理函数;
  4. 路由分发到UserController :
    • 找到注册的登录处理函数 UserController::login();
    • 将解析后的 HttpRequest 对象传递给该函数;
  5. UserController处理业务逻辑 :
    • 解析请求体中的用户名和密码;
    • 调用 DbUtil 查询数据库验证用户凭证;
    • 验证通过后,调用 AuthUtil::generateToken() 生成Token;
    • 调用 AuthUtil::registerToken() 注册Token信息;
    • 构建包含Token和用户信息的响应对象;
  6. 返回响应 :
    • ConnectionHandler::buildResponse() 方法构建HTTP响应;
    • 通过Socket发送响应数据给客户端;
    • 关闭连接或等待下一个请求;

六、核心接口设计

6.1 接口规范

  • 请求头:Content-Type: application/json、Authorization: Bearer {Token};
  • 响应格式:{code: 状态码, msg: 提示信息, data: 业务数据};
  • 跨域处理:Qt6 下响应头添加 Ubuntu 兼容的跨域字段。

6.2 核心接口列表

(1)用户相关接口
接口名称 请求方式 接口路径 权限要求 核心参数 状态码说明
用户登录 POST /api/login username, password 1001 = 账号密码错误,1004 = 参数为空
获取用户信息 GET /api/user/info 所有登录用户 token(请求头) 1003=Token 无效
新增用户 POST /api/user 管理员 / 超级管理员 username, password, role 1002 = 无权限,1004 = 用户名已存在
删除用户 DELETE /api/user 管理员 / 超级管理员 id(URL 参数) 1002 = 禁止删除自身,1005 = 用户不存在
获取用户列表 GET /api/user/list 管理员 / 超级管理员 token(请求头) 1002 = 无权限
重置用户密码 PUT /api/user/reset 管理员 / 超级管理员 id, new_password 1007 = 无权重置,1008 = 不符合密码规则密码
修改自身密码 PUT /api/user/password 所有登录用户 old_password, new_password 1009 = 原密码错误,1008 = 密码长度不足,1003=Token 无效
(2)物料相关接口
接口名称 请求方式 接口路径 权限要求 核心参数 状态码说明
新增物料 POST /api/material 管理员 / 超级管理员 name, supplier, stock_num 等 1002 = 无权限,1004 = 必填字段为空
修改物料 PUT /api/material 管理员 / 超级管理员 id(URL 参数) 1005 = 物料不存在
删除物料 DELETE /api/material 管理员 / 超级管理员 id(URL 参数) 1002 = 无权限,1005 = 物料不存在
物料入库 PUT /api/material/in 管理员 / 超级管理员 id, num 1004 = 数量≤0,1005 = 物料不存在
物料出库 PUT /api/material/out 管理员 / 超级管理员 id, num 1006 = 库存不足
物料查询 GET /api/material/search 所有用户 keyword(可选)
物料详情 GET /api/material 所有用户 id(URL 参数) 1005 = 物料不存在

6.3 跨域处理(这里得注意)

cpp 复制代码
    if (m_enableCors) {
        responseHeaders += QString("Access-Control-Allow-Origin: %1\r\n").arg(m_allowOrigin).toUtf8();
        responseHeaders += "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n";
        responseHeaders += "Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Origin, Authorization, X-Requested-With\r\n";
        responseHeaders += "Access-Control-Expose-Headers: Content-Length, Content-Type\r\n";
    }

接下来就是编码,等前后端编码完成,测试时需要注意一些部署问题。

七、部署注意

7.1 后端服务部署

步骤 1:通用依赖安装(所有环境必装)

bash 复制代码
# 更新系统源
sudo apt update

# 1. 安装Nginx(前端部署核心)
sudo apt install nginx -y

# 2. 安装基础工具(调试/运维用)
sudo apt install sqlite3 net-tools ufw -y

# 3. 开放端口(80/8798)
sudo ufw enable
sudo ufw allow 80/tcp
sudo ufw allow 8798/tcp
sudo ufw reload

步骤 2:启动后端服务(虚拟机内自测,便于查看日志)

bash 复制代码
# 进入后端程序目录
cd /home/zz/erp-deploy/server
#先清理可能存在的进程
sudo kill -9 $(ps -ef | grep MaterManageServer | grep -v grep | awk '{print $2}') 2>/dev/null
#拷贝程序到目录
cp /home/zz/ZZItem/MaterManage/MaterManageServer/build/Desktop_Qt_6_5_3_GCC_64bit-Release/MaterManageServer /home/zz/erp-deploy/server/
#赋予执行权限
chmod +x /home/zz/erp-deploy/server/MaterManageServer
# 直接启动后端程序(开发环境Qt库已存在,无需额外配置)
./MaterManageServer

正常就会输出日志(具体看你打的日志):

备注:保持终端窗口打开,关闭则服务停止;若需后台运行,执行

bash 复制代码
nohup ./MaterManageServer > server.log 2>&1 &

步骤 3:后端服务验证(自测)

bash 复制代码
# 检查端口是否监听
netstat -tulpn | grep 8798

# 测试登录接口(验证服务可用性)
curl -X POST http://127.0.0.1:8798/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"71dc9f89190a48d2a8afcb59d703275a"}'

得到响应:

7.2 前端页面部署

步骤 1:修改 Nginx 配置

bash 复制代码
# 备份原有Nginx配置
sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak

# 编辑Nginx配置文件
sudo vim /etc/nginx/sites-available/default

替换为以下内容(重点匹配前端目录名 MaterManagerFront):

bash 复制代码
# Nginx服务器配置
server {
    # 监听80端口(默认HTTP端口,直接访问IP/域名即可)
    listen 80;
    listen [::]:80;

    # 前端静态资源根目录
    root /home/zz/erp-deploy/frontend/MaterManagerFront;

    # 配置入口HTML文件(指定html子文件夹下的所有入口文件)
    # 优先级从左到右,优先匹配index.html,依次往下
    index html/index.html html/login.html html/material-manage.html html/personal-info.html html/user-manage.html;

    # 服务器名称(本地部署留空或填虚拟机IP/localhost)
    server_name _;

    # 托管前端静态资源+适配前端路由刷新
    location / {
        # 尝试查找请求的文件 → 尝试查找请求目录 → 兜底返回html/index.html(解决路由刷新404)
        # 确保直接访问IP时,能自动定位到html/index.html;访问/login.html时,定位到html/login.html
        try_files $uri $uri/ /html/index.html;
        # 允许所有请求访问
        allow all;
    }

    # 反向代理(解决前端调用后台8798端口跨域)
    location /api/ {
        proxy_pass http://127.0.0.1:8798/api/;
        # 传递客户端真实IP和请求头
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # 静态资源缓存配置(无需修改,已适配css/js/res在根目录的结构)
    location ~* \.(css|js|png|jpg|jpeg|gif|ico|res)$ {
        # 缓存有效期7天,提升访问速度
        expires 7d;
        # 关闭日志记录,减少冗余
        access_log off;
    }

    # 直接定位HTML文件的优化配置(方便前端跳转,不加也行)
    location ~* \.html$ {
        # 优先从html子文件夹查找HTML文件
        try_files $uri /html/$uri /html/index.html;
        expires -1; # HTML文件不缓存,确保修改后实时生效
    }
}

步骤 2:重启 Nginx 并验证

bash 复制代码
# 检查配置语法
sudo nginx -t

# 重启Nginx
sudo systemctl restart nginx

# 检查Nginx状态
sudo systemctl status nginx

7.3 外部客户机部署

外部客户机通常无 Qt 开发环境,需解决 MaterManageServer 的 Qt 库依赖问题,提供两种方案(按需选择)。
方案 1:动态依赖打包(推荐,体积小)

核心思路

将程序依赖的 Qt 动态库复制到客户机,通过 LD_LIBRARY_PATH 指定库路径,无需安装 Qt 环境。

步骤 1:在开发环境提取依赖库

bash 复制代码
# 1. 创建库目录
mkdir -p /home/zz/erp-deploy/server/lib

# 2. 提取MaterManageServer依赖的Qt库(使用ldd命令)
ldd /home/zz/erp-deploy/server/MaterManageServer | grep "Qt6" | awk '{print $3}' | xargs -I {} cp {} /home/zz/erp-deploy/server/lib/

# 3. 提取其他系统依赖(若客户机缺失)
ldd /home/zz/erp-deploy/server/MaterManageServer | grep -E "libc.so|libm.so|libpthread.so" | awk '{print $3}' | xargs -I {} cp {} /home/zz/erp-deploy/server/lib/

# 4. 整理依赖包(打包server目录)
cd /home/zz/erp-deploy/
tar -zcvf MaterManageServer-deploy.tar.gz server/

步骤 2:客户机部署依赖包

bash 复制代码
# 1. 将打包文件上传到客户机(比如用scp)
scp /home/zz/erp-deploy/MaterManageServer-deploy.tar.gz root@客户机IP:/opt/

# 2. 客户机解压
mkdir -p /opt/erp/server
tar -zxvf /opt/MaterManageServer-deploy.tar.gz -C /opt/erp/

# 3. 启动服务(指定库路径)
cd /opt/erp/server
export LD_LIBRARY_PATH=$PWD/lib:$LD_LIBRARY_PATH
chmod +x MaterManageServer
nohup ./MaterManageServer > server.log 2>&1 &

方案 2:静态编译(无依赖,体积大)

核心思路

编译 MaterManageServer 时,将 Qt 库静态链接到程序中,生成单文件可执行程序,客户机无需任何依赖。

步骤 1:开发环境静态编译配置

打开 Qt Creator,加载 MaterManageServer 项目;

进入「项目」→「构建配置」→「CMake」;

添加 CMake 参数:

bash 复制代码
-DCMAKE_BUILD_TYPE=Release
-DQT_STATIC=ON
-DCMAKE_EXE_LINKER_FLAGS="-static -static-libgcc -static-libstdc++"

重新编译项目,生成静态版 MaterManageServer(体积约 50-100MB)。

步骤 2:客户机部署静态程序

bash 复制代码
# 1. 上传静态程序到客户机
scp /home/zz/ZZItem/MaterManage/build-static/MaterManageServer root@客户机IP:/opt/erp/server/

# 2. 赋予执行权限并启动
chmod +x /opt/erp/server/MaterManageServer
nohup /opt/erp/server/MaterManageServer > /opt/erp/server/server.log 2>&1 &

依赖验证:

动态包方案:检查 LD_LIBRARY_PATH 是否生效,ldd MaterManageServer 无缺失库;

静态编译方案:ldd MaterManageServer 显示 not a dynamic executable(正常)。

外部客户机前端部署:这里与在自己开发机自测部署一样,只需要修改config.js文件为客户机的对应IP地址就行(不再赘述)。

总结

该架构设计核心时基于 Qt6.5 与 Ubuntu24.04 的开发环境下,实现一个简单的物料管理系统,并解决路由分发,密码安全、数据库规范使用,并发处理,跨域处理等常见核心能力。架构在保持轻量化部署的基础上,确保了系统的稳定性和兼容性,适合小微企业在 Linux 环境下部署使用,可根据实际需求进一步扩展功能或优化性能。

相关推荐
笨笨马甲3 小时前
Qt TCP连接硬件设备
开发语言·qt·tcp/ip
teacher伟大光荣且正确3 小时前
关于Qt QReadWriteLock(读写锁) 以及 QSettings 使用的问题
java·数据库·qt
Larry_Yanan7 小时前
Qt多进程(四)QTcpSocket
开发语言·c++·qt·ui
CC.GG7 小时前
【Qt】常用控件----QWidget属性
java·数据库·qt
hqwest7 小时前
码上通QT实战02--登录设计
开发语言·qt·登录·ui设计·qt控件·qt布局·qt登录
世转神风-7 小时前
qt-文件自动按编号命名
开发语言·qt
世转神风-8 小时前
qt-float转QByteArray-二进制存储-数据存储(IEEE 754标准)
开发语言·qt
EverestVIP9 小时前
Qt 信号槽断开连接的几种方式
开发语言·qt
小c君tt9 小时前
QT中treewidget中右键添加QAction方法
开发语言·qt