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


开发环境: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)调用流程为例,完整的调用流程如下:
- 前端发起请求 :
- 发送POST请求到 http://服务器IP:8798/api/login
- 请求体包含用户名和密码的JSON数据
- HttpServer接收连接 :
- HttpServer::incomingConnection() 方法接收到新的TCP连接;
- 创建 ConnectionHandler 实例并提交到线程池执行;
- ConnectionHandler处理请求 :
- ConnectionHandler::run() 方法初始化Socket和事件循环;
- ConnectionHandler::onReadyRead() 方法读取请求数据;
- ConnectionHandler::parseRequest() 方法解析HTTP请求,提取方法、路径、请求体等信息;
- ConnectionHandler::dispatchRoute() 方法根据请求方法和路径查找对应的路由处理函数;
- 路由分发到UserController :
- 找到注册的登录处理函数 UserController::login();
- 将解析后的 HttpRequest 对象传递给该函数;
- UserController处理业务逻辑 :
- 解析请求体中的用户名和密码;
- 调用 DbUtil 查询数据库验证用户凭证;
- 验证通过后,调用 AuthUtil::generateToken() 生成Token;
- 调用 AuthUtil::registerToken() 注册Token信息;
- 构建包含Token和用户信息的响应对象;
- 返回响应 :
- 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 环境下部署使用,可根据实际需求进一步扩展功能或优化性能。