酒店管理系统
一、搭建环境
- Qt:5.14.2 mingw73_64
- MySQL:MySQL 8.0.22
想要源码的可以直接去github拿额:地址
二、项目模块
项目模块主要是以下三个模块
- 前台预定/开房
- 财务结算
- 系统基础数据维护
现在的开发进度完成了 登录、系统基础数据维护模块,开发完房态图,下面还有一些就不准备做了,因为去工作了,哈哈哈。
目前我已经把该项目的数据库发布到了网上,可以通过网络进行访问数据库。
三、开发记录/感想
UI接近网页的风格,走简约风格,主要就是灰色调,使用蓝色作为交互颜色,还有蓝色的对比色”黄色“,为选中表格的颜色。
还有一些模块的技术带你没来得及记录或者自己难以用文字描述出来…
3.1项目结构
DB文件夹:放置有关数据库的类
- SqlDatabase 类存放数据库基本信息
(1)数据库对象
(2)驱动名称
(3)数据库名称
(4)数据库地址
(5)数据库用户名
(6)数据库用户密码
(7)端口 - SqlHelper 连接/关闭数据库
- SqlInterface 操作数据库函数的基类
Dao文件夹:BaseDao 、SpeedyDao类接收Service 层传递过来的字段和值,进行拼接SQL语句,调用SqlInterface中的函数进行操作数据库。
Service文件夹:每一个Service类都有之对应的界面/交互功能类,接收来自界面/交互功能类的参数,把参数封装成一个适合Dao层参数,调用Dao层操作数据库。
Widgets 文件夹:界面类存放的位置
3.2 登录
3.2.1知识点
- 连接数据库:QSqlDatabase
- 查询方法:QSqlQuery、QSqlRecord、QSqlError
- 文件的导出与导入:QFile、QTextStream
- 错误提示:QMessageBox
- 事件:showEvent
- 背景颜色:setPalette函数和QPalette
3.2.2知识点精讲(SQL)
登陆中使用到了一个查询函数(SqlInterface层),它的声明如下:
QList<QVariantMap> queryList(QString sql,QVariantList params = QVariantList());
实现逻辑:
- 判断数据库是否开启。
- 使用QSqlQuery类的prepare函数加载sql语句。
- 使用QSqlQuery类的bindValue函数绑定值。
- 使用QSqlQuery的exec函数查询。
- 使用一个whlie循环,后使用QSqlQuery的next函数作为循环因子,进行遍历查询出的数据。
- 将查询好的数据添加到QList 集合中
- 返回该集合
3.2.3文字介绍
软件启动后,显示登录对话框
输入用户名和密码后,点击登录;这时候进入数据库查询用户信息,匹配则导出一个文本,里面写入用户名称,后打开主页面。若不匹配,则提示,如下图。
3.3 主页面
主页面由三个子widget组成
- HeaderWidget :头部信息(logo、登录信息、退出登录)
- ModelTreeWidget:模块树形列表
- CentreWidget:中心控件
3.3.1知识点
- 代码布局
- 计时器(时间显示):QTimer
- 跨页面的信号和槽:Widget中的 ModelTreeWidget和CentreWidget 两个类的交互
3.3.2 知识点精讲(信号和槽)
通过中间件Widget使用信号和槽进行跨页面交互,实现的功能:
- 点击ModelTreeWidget中的一项,触发itemClicked 信号。
- 调用ModelTreeWidget::onTreeWidgetItemClicked槽,在该槽中发送自定义信号void treeWidgetItemClicked(QString widgetName)。
- 在Widget中将ModelTreeWidget的treeWidgetItemClicked信号与CentreWidget 中的槽函数addTabWidgetInName进行连接。
- 这时候就构成了通过一个中间件,进行跨页面传输。
3.3.3 ModelTreeWidget
它的中心控件为TreeWidget,用于显示从数据库中查询到的系统模块。
3.3.3.1知识点
- TreeWidget迭代器的使用:QTreeWidgetItemIterator
- 自定义信号
2.3 文字介绍
进入到登录页面后HeaderWidget部分的右上角,将显示当前登录者,显示登录者右边是注销按钮,点击它将会弹出提示是否注销登录,是,就注销账号后重启软件,否,不进行操作。
在左边的树形列表中点击一项,将在右边的窗口中添加该项对应的窗体(图片看不见建议放大看…)。
3.3.4 房态
房态:空闲(绿色)、预约(蓝色)、入住(灰色)、清洁(黄色)、红色(维修)
鼠标滚轮向下滚动到一定的位置就加载剩下的,鼠标放在房间块上显示房间的具体信息,右键可以直接预定啥的,功能没写,但是样式写上去了。这里有点小bug啦…
这里显示数据的是 视图类(QGraphicsView)视图项也是自己定义的。
3.4 系统设置
3.4.1知识点
- 使用半模态对话框。
- 新增或修改时,数据库已有该数据,将不会重复添加或修改。
- 查询一个表的数据,不存在另外一个表中的查询(两表之间是有关系的)
- 关于QSqlQuery模糊查询的问题(修复模糊查询bug)
3.4.2 知识点精讲
3.4.2.1 SQL
上方知识点中的第二点,便不会重复添加或修改数据是通过SQL语句的NOT EXISTS关键字完成任务的。
实现逻辑:
- 具体页面层调用调用Service层,Service层调用Dao层,Dao层调用DB层SqlInterface类
- Dao层的函数声明:
bool upDataByInfoRepeat(QString tableName,QStringList key = QStringList(), QVariantList value = QVariantList(), QHash<QString,QList<QVariant>>hash = QHash<QString,QList<QVariant>>());
- 它的第三个参数hash ,hash的第一个值必须得是ID。
3.4.2.2自定义model(员工)
关于CustomTableModel,重写了以下虚函数
- rowCount:返回当前model存储数据的行数
- columnCount:返回当前model存储数据的列数
- data:为tableView设置数据,以及数据显示方式
- setData:当tableView的数据发生改变时,修改model中的数据
- flags:每一个项的控制(是否可选中等)
- headerData:根据参数,返回水平/垂直的第n个表头列。
在这个model中,用于保存数据的容器是:
- QStringList m_header; //表头
- QVariantList m_data; //数据
- QMap<int,QVariant> m_userRoleData; //UserRole的数据
初始化数据时候,从QList中取出数据。调用的方法是:SpeedyDao::selectSqlRecord
由于这个自定义model做的逻辑有些混乱,所以做完这个model后准备弃用。
一开始我的想法是,model与service两个层进行交互查询SQL拼接和查询都交给service层来做,但是没有实现两者之间的分层。为了完成这个model,两者之间的关系发生了一些混乱的交互,逻辑也开始混乱。
现在就开始捋一捋model、service、ui这三个层之间的运作逻辑(转至3.3.3员工部分观看)。
3.4.2.3自定义委托控件
在3.4.2.2员工中使用了自定义委托控件(Delegate文件夹下),它是继承自QItemDelegate的一个委托类,重写了以下函数:
- paint:根据option.state进行不同状态下控件的绘制
- createEditor:创建控件,控件一般在这里进行初始化
- setEditorData:当tableview中的委托控件项数据发生改变后,修改当前控件中的数据
- setModelData:当tableview中的委托控件项数据发生改变后,修改model中的数据
3.4.3 员工管理
3.4.3.1部门
基础数据的增删查改,但是在新增和修改的过程中数据库中出现相同的数据,那么本次新增或修改将不会执行。
该部门展示数据的控件是QTableView,同时还使用了QSqlTableModel,调用QSqlTableModel的setTable函数指定查询表。
QSqlTableModel是一个可以直接在表格中双击进行编辑的Model,这个功能实在是太强大了。所以我给他双击编辑这个功能需要使用提交按钮才能提交,当觉得修改不好或发生一些无法预知的问题,可以使用“回退”进行数据的恢复。(没有使用“提交”的情况下才可以使用“回退”功能)。
3.4.3.2 职业
基础数据的增删查改。在新增和修改的过程中数据库中出现相同的数据,那么本次新增或修改将不会执行。
该页面展示数据的控件是 QTableWidget,项 QTableWidgetItem 。使用手动添加的方法进行添加。
在这里我用于存储数据的容器是实体类Entitys/SystemSimpleModelEntity.h文件中 StaffPosition
3.4.3.3 员工
这个页面是员工管理的精髓页面,同时也是最难的一个。在这里没有使用到新增和修改重复验证这个功能。
该页面中展示数据的控件是QTableView,使用了一个自定义的Model(Custom/CustomTableModel.h)。用到了分页功能,还有tableView直接编辑功能,自定义委托等。
该页面实现了的功能点:
- 显示数据,使用自定义model和QTableView进行显示
- 分页
- 根据宽度来自动调节每一列显示的宽度,根据高度来自动调节显示多少行
- 弹出对话框进行新增/修改
- 删除
- 直接编辑QTableView 进行修改,修改没问题后“提交”,有问题“回退”
model、service、ui三层运作逻辑:
- 查询:ui层:初始化service、委托、私有成员变量(从service获取)
(2)service初始化model(初始化了model的sql查询语句)、PagingClass分页、下拉框service、还初始化了该类的私有成员变量(列数、开始索引等)
(3)ui层调用了resizeEvent事件函数,在这里加载model数据、tableview设置model、委托
(4)ui调用service->setLimit函数设置数据查询条数,给model设置查询sql。service->select函数里面设置了分页总条数和调用了model的setsql函数
(5)ui条用service->setLimit后紧接着调用service->select函数,select函数调用model的select函数加载数据,同时调用分页的setDataCount设置总页数
(6)ui开始设置model给tableview,委托给tableview。
(7)下一页功能等,调用的是service->setPage函数,setPage函数中调用分页的setThisPage函数,开始判断是否复合条件复合条件发送tableUpdate信号,service层pageChanged槽接收,槽中使用了service->setLimit和service->select函数;setThisPage函数判断不符合时发送pageNumInvalid信号,在service的onPageError槽中进行转发至ui页面他,ui页面将提示该error - 新增:ui层:弹出对话框,填写数据,点击新增,检查完整性,对话框发送信号,ui接收调用service->insertData方法
(2)service层:拼接sql,拼接值,调用dao->insertSql方法
(3)service层开始返回,true | false
(4)ui层判断返回值,true成功,false失败。 - 修改(直接编辑QTableView)
(1)ui层:直接编辑,根据不同的委托不同的控件即不同的编辑方式,调用委托类setEditorData和setModelData方法
(2)委托类:setEditorData修改控件数据,setModelData调用model->setData方法修改模型数据
(3)model:根据role修改model中的数据,当修改后会发送一个信号updateToService把编辑行id、修改字段、修改值,这个信号将会在service中的onModelUpdateAppendSql槽接收。这个槽函数主要是将我们从model传来的数据保存到易容临时容器中,当ui点击提交,调用service->commitData函数进行循环修改。
(4)保存修改数据的临时容器是
①QMap<int,int> m_updateMap; 第一个int是行ID,第二个int是第n条数据
②QList<QList > m_updateField; 1中的第二个int代表最外层的list索引,list中的list为QString类型,为字段组。
③QList<QList > m_updateValues; 与上方2类似。
由于为了实现上面介绍的功能,为此修改了modle、service、paging分页三者。它们之间的关联性实在是太过于强内聚,没有很好进行解耦。而且用起来还特别麻烦,需要定义多个方法与之呼应,这个写法显然不是我所需要的。所以CustomTableModel(model)、PagingClass(分页)两个类归属为staff这个类独有的。在往后的表格编辑中会新建一个全新的,更易于扩展的model和分页。
上面修改(直接编辑QTableView)model往后的解析太牵强,直接上代码:
/**
* @brief StaffService::onModelUpdateAppendSql
* @param id 员工id
* @param field 修改列
* @param value 修改值
* 接收来自 CustomTableModel 修改信号
*/
void StaffService::onModelUpdateAppendSql(int id, QString field, QVariant value)
{
//判断map中是否存在该id
if(m_updateMap.contains(id))
{
//存在追加
int index = m_updateMap.value(id);
QList<QString> fields = m_updateField.at(index);
QString fieldMyName = fieldName(field);
if(!fields.contains(fieldMyName))
{
m_updateField[index].append(fieldMyName);
m_updateValues[index].append(value);
}
else
{
int repIndex = fields.indexOf(fieldMyName);
(m_updateField[index])[repIndex] = fieldMyName;
(m_updateValues[index])[repIndex] = value;
}
}
else
{
//不存在,添加
m_updateMap.insert(id,m_index);
QList<QString> fields;
fields.append(fieldName(field));
m_updateField.append(fields);
QVariantList values;
values.append(value);
m_updateValues.append(values);
m_index++;
}
}
3.4.3.4 操作员
操作员这个界面用到了4张表:1.操作员表2.员工表3.部门表4.职业表.
操作员左连接员工,员工左连接部门和职业表。
在这里新增的时候遇到了一个有趣的查询,查询员工表,查询不是操作员的员工。这里我封装的拼接SQL语句方法弄得我够呛…用到了模糊查询和Not Exists(不存在),然后发现给Select加上Not Exists 写法太过于臃肿,然后我把它改成了 Not IN
图片中左边的带有查询的表格,那就是上方说到的“查询员工表,查询不是操作员的员工”,它是通过右边的对话框点击“选择员工”按钮弹出的。
-
关于QSqlQuery的模糊查询产生一个BUG。我们拼接好SQL语句后给到QSqlQuery进行绑定参数如:
SELECT room.room_num FROM room WHERE room.room_num LIKE ‘%?%’
上面这句假设就是我们拼接的SQL语句,我们直接使用QSqlQuery进行绑定参数,这个时候进行exec()查询后,发现一条数据都莫得。这就是我们在绑定参数的时候产生的一个bug,最神奇的我们使用boundValues()函数获取绑定的参数值,它居然绑定上去了…在这里我卡了半天。 -
问题产生的原因:在网上大佬是这样说的,绑定string时会自动加上单引号所以%必须在bindValue()时候接上去。
-
解决方案:bindValue()函数中添加%符号。
m_query->bindValue(i,QString("%%1%").arg(p.toString()));
关于查询:(臃肿)
NOT EXISTS (SELECT 1 FROM ( SELECT 1 FROM operator AS d1 WHERE d1.staff_id = staff.staff_id) AS a)
良好的:
staff.staff_id NOT IN (SELECT operator.staff_id FROM operator )
3.4.4 房间管理
房间管理有:楼区、楼座、房间类型、房间状态、房间属性、房间、房间拥有的属性,总共7个小模块。
它们之间有许多相同的代码,我就不一一介绍了,下方把逻辑差不多的页面进行整理。
它们是:楼区、楼座、房间类型、房间状态、房间属性,这5个小模块。就是基础数据部分,没有太多的难点,并且为了加快开发速度,统一使用了QTableWidget这个容器,然后手动添加item(QTableWidgetItem)。下面一个一个截图看一下吧。
在这个模块中着重讲解 房间 、房间拥有的属性这两个页面。
3.4.4.1 楼区
3.4.4.2 楼座
3.4.4.3 房间类型
3.4.4.4 房间状态
3.4.4.5 房间属性
3.4.4.6 房间
在房间管理这一部分,我们使用了新的自定义model和分页。
查询部分:查询部分使用了当窗体大小改变,表格显示的条数会根据tableView的高度而改变。当然我们可以手动设置显示条数。首页、上一页、下一页、尾页和跳转这些基础的分页功能我们都有,当然不仅如此,我们提供了更加精确的条件查询。下面一起看一下我们的UI界面吧。
查询:
一开始我们初始化查询,是不带任何筛选条件的,显示条数根据页面的大小而改变。
条件查询对话框,是我们用来具体筛选那些数据是需要查询的,筛选数据一般离不开like模糊查询,在操作员那一部分讲解了like的使用,可以回头看一下。我将对话框中得到的数据它放到QList容器进行一些处理:将数据为空的数据剔除,然后通过发送信号传输到我们的房间主页面中。
然后通过房间主页面创建一个槽函数接收该信号传输过来的信号,放入service层中进行处理拼接SQL,在条件中like用到的情况下我们在service发送一个like出现的索引位置,传输到dao层绑定参数会用到这个索引。
我们的房间主页面、service、条件对话框,只是用来处理查询数据拼接SQL语句。真正的查询是在我们最新创建的model中:CustomTableViewModel
CustomTableViewModel 是一个集成新增、修改、分页的model,这些实现操作都是与该类有关。相对于之前我们自定义的 CustomTableModel 它逻辑更加的独立、清晰,不需要在service中创建一大堆的槽函数,来实现model与分页的交互,更加轻便。
setSql:设置sql语句
select:查询
setShowCount:设置显示条数
setThisPage:设置当前页,跳转
新增:
新增之前我们需要给model设置一个新增的SQL语句,通过setInsertSql方法。我们通过insertRows方法在model中创建一行全新的空白行,这时为新增状态(普通按钮将隐藏,显示提交/回滚按钮),在setData中实现新增的真正操作,这一步实在是太复杂了,想简化。
直接贴代码:
if(!index.isValid())
{
return false;
}
int row = index.row();
int col = index.column();
if(value == m_data[row][col])
{
return true;
}
if(role == Qt::EditRole)
{
m_data[row][col] = value;
}
else if(role == Qt::UserRole)
{
//获取当前列名
QString field = m_headerData.at(col);
//新增
if(m_isInsert)
{
if(m_updateData.size() > 0)
{
//添加至容器
for(int i = 0; i < m_updateData.size(); i++)
{
QVariantMap map = m_updateData[i];
//存在
QMap<QString,QVariant>::iterator ite = map.find(field);
if(ite != map.end())
{
ite->setValue(value);
break;
}
else
{
QVariantMap map1;
//不存在
map1.insert(field,value);
m_updateData.append(map1);
break;
}
}
//排序,新增
if(m_headerData.size()-1 == m_updateData.size())
{
QVariantList myvalue;
//排序
for(int i = 0; i < m_headerData.size(); i++)
{
for(int j = 0; j < m_headerData.size()-1; j++)
{
QMap<QString,QVariant>::iterator ite = m_updateData[j].find(m_headerData[i]);
if(ite != m_updateData[j].end())
{
myvalue.append(ite.value());
break;
}
}
}
//新增
if(!m_speedDao->insertSql(m_insertSql,myvalue))
{
emit sqlError("新增发生错误!" + m_speedDao->getLastError());
backAll();
return false;
}
else
{
m_isFirstSelect = true;
m_updateData.clear();
}
}
}
else
{
//不存在
QVariantMap map;
map.insert(field,value);
m_updateData.append(map);
}
}
//下面还有个else,在下方...
修改:
修改需要使用到m_headerData中的字段进行拼接SQL和绑定参数,实现也是在setData中,但是比新增代码少很多,它跟在新增的后面就是它的else部分
贴代码:
else
{
//修改 1.修改的表 2.修改的字段 3.表主键
QString updateSql = QString(" UPDATE %1 SET %2 = ? WHERE %3 = ? ").arg(m_tableName)
.arg(field).arg(m_headerData.at(0));
QVariantList values ;
values << value << index.sibling(index.row(),0).data();
if(!m_speedDao->updateToModel(updateSql,values))
{
qDebug() << m_speedDao->getLastError();
emit sqlError("修改发生错误!");
backAll();
return false;
}
}
m_userData[row][col] = value;
}
删除就是在房间主页面和service中实现的,没有把删除括入model的范围。
3.4.4.7 房间拥有的属性
在这里我们使用到了model、service、筛选对话框,因为这里我们需要查询房间信息,那么直接把之前的东西拿来用。当然还有我们房间属性表中,我们也是拿之前的东西来用这样一来就可以减少好多的工作量。
新增部分就有一个新的东西,在新增前查询一下有没有重复。(根据条数>0,说明重复)
其它的倒也没什么了,下面看一下UI界面吧。
3.4.5 其它
3.4.5.1 流水号管理
没有什么新的东西
都快成说明书了,哈哈哈…