简介:简易人事管理系统是一套基于PyQt5与MySQL的桌面端示例项目,面向正在学习Python界面开发、数据库集成或准备课程设计的高校学生与初级开发者,可帮助解决传统手工维护员工档案、工资记录效率低的问题。系统覆盖登录验证、员工档案、部门管理、薪资记录与变动查询等常见功能,能帮助理解一个小型ERP模块从建库到界面呈现的完整流程。压缩包共49个文件,主体为40个Python源文件(含界面、业务与数据库测试脚本),另有7张PNG界面素材、1个SQL建库脚本和1份README说明文档,整体仅295KB,结构清晰、模块划分明确。目前已有2689人学习下载。通过阅读相互调用的类与方法,读者可掌握PyQt5信号槽、MySQL增删改查以及模块化开发的配合方式,快速获得可运行、可改造的人事管理原型,也可直接用于课程设计或毕业设计。
1. 简易人事管理系统:用 PyQt5 和 MySQL 趟出一条桌面端落地路径
如果你接过桌面端小系统的私活,多半对“人事管理”这四个字又爱又恨:逻辑不算复杂,无非增删改查、部门统计、账号权限,但需求方永远会追加“导出 Excel”“支持模糊搜索”“别动不动就卡死”。这套简易人事管理系统用 PyQt5 做界面、MySQL 存数据,把“登录鉴权 + 员工信息管理 + 部门统计 + 操作记录”完整走通。适合正在学 PyQt5 的学生、准备用 Python 做毕业设计的开发者,以及想快速搭一套内部工具给中小团队用的从业者。项目思路并不高深,它真正值钱的地方在于:界面层和数据层怎么拆,哪些坑会浪费你一下午。
2. 登录模块:界面与数据库校验的首次握手
2.1 为什么先做登录墙,而不是直接铺员工列表
人事系统的数据敏感度比较高,工资、手机号、身份证号都在里面。如果不做权限控制,后面验收交付时需求方大概率让你返工。登录模块在很多毕设里只是摆个样子,但这套项目把登录接口做成了“界面输入 → 服务层校验 → 数据库反查 → 会话缓存”的标准链路,后续扩展两个角色也方便。
常见做法是点击登录按钮后,前端把用户名和密码通过 PyQt5 信号交给业务层,业务层对密码做 MD5 加盐处理,再拿用户名去 MySQL 查询。之所以不直接 SELECT * FROM user WHERE username = xxx AND password = xxx,是因为明文密码落库风险太大,而且这种写法把 SQL 语句和界面耦合在一起,改表结构时很痛苦。
2.2 登录模块实现:从 QLineEdit 到数据校验
下面是一段登录按钮槽函数的精简实现,对应普通用户和管理员的区分逻辑:
from PyQt5.QtWidgets import QMessageBox from hashlib import md5 def on_login_clicked(self): username = self.ui.edit_user.text().strip() password = self.ui.edit_pwd.text().strip() if not username or not password: QMessageBox.warning(self, "提示", "用户名和密码不能为空") return hashed = md5((password + self.salt).encode("utf-8")).hexdigest() result = self.auth_service.verify_login(username, hashed) if result["success"]: self.current_user = result["role"] self.open_main_window() else: QMessageBox.critical(self, "错误", result["message"])逻辑说明:先对输入做去空格校验,避免用户误触登录按钮导致空请求;随后对密码做 MD5 加盐哈希,salt 通常放在配置文件中,不进数据库,防止撞库攻击时直接拿散列值比对;最后调用auth_service.verify_login(),由它去执行参数化查询。
参数说明:self.salt是项目初始化时从配置文件读取的随机字符串,如果需求方要求极高安全性,可以把 MD5 换成 bcrypt 或 sha256 + 随机盐,但毕设或中小系统 MD5 加盐足够。verify_login返回字典里带role字段,用来控制菜单项显隐,比如管理员能看到“操作记录”菜单,普通用户只能看自己的资料。
3. 员工信息管理:QTableWidget 与 SQL 增删改查的配合
3.1 为什么用 QTableWidget 而不是 QTableView + Model
员工列表是人事系统用得最多的页面。初学者常纠结表格控件选哪个:QTableWidget 是直接往单元格塞数据的封装控件,适合数据量几千行以内的中小系统;QTableView 配合 QSqlTableModel 适合大数据量分页场景,但需要额外写代理和排序逻辑,复杂度高不少。
这套项目选 QTableWidget,原因很简单:员工表最多几千行,QTableWidget 的 setItem 方式可以直接控制单元格内容,查出来一条记录就塞一行,修改时直接读单元格,开发效率最高。如果以后员工量涨到几万,再换 QTableView 也不迟,表结构不需要变。
3.2 员工查询与表格填充:状态就是界面本身
搜索和刷新是高频操作。常见做法是界面上放 QLineEdit 做关键字输入,QComboBox 选部门筛选,点查询后调 service 层,把结果集塞回表格:
def refresh_table(self, keyword=None, dept_id=None): rows = self.employee_service.query_employees(keyword, dept_id) self.ui.table_emp.setRowCount(len(rows)) for row_idx, emp in enumerate(rows): item_id = QTableWidgetItem(str(emp["emp_id"])) item_id.setFlags(item_id.flags() & ~Qt.ItemIsEditable) self.ui.table_emp.setItem(row_idx, 0, item_id) self.ui.table_emp.setItem(row_idx, 1, QTableWidgetItem(emp["name"])) self.ui.table_emp.setItem(row_idx, 2, QTableWidgetItem(emp["dept_name"])) self.ui.table_emp.setItem(row_idx, 3, QTableWidgetItem(emp["phone"])) # 部门列隐藏,后续统计用 self.ui.table_emp.item(row_idx, 2).setData(Qt.UserRole, emp["dept_id"])逻辑说明:这段刷新方法直接体现“视图即状态”思路。每次查询都重建表格内容,而不是尝试增量更新,数据量不大时干净利落。主键列被设为不可编辑,防止用户误改 ID;部门名称虽然显示在表里,但真正的dept_id存在Qt.UserRole里,后续做部门统计或编辑时直接取不会丢。
参数说明:keyword支持姓名模糊匹配,内部用LIKE CONCAT('%', %s, '%')拼接参数化占位符;dept_id为 -1 时表示不筛选部门,避免写两条几乎一样的 SQL。刷新完成后清空 QLineEdit 焦点,避免用户按回车重复触发查询。
3.3 新增员工与弹窗表单:复杂字段先做前端校验
新增和编辑不能直接在表格里操作,因为员工信息有性别、入职日期、部门、学历等多个字段,表格塞不下。项目里用一个 QDialog 弹窗承载 QFormLayout 表单,这类弹窗在管理系统里是通用套路:
class EmpEditDialog(QDialog): def __init__(self, parent=None, emp_data=None): super().__init__(parent) self.emp_data = emp_data self.ui = Ui_EmpDialog() self.ui.setupUi(self) if emp_data: self.setWindowTitle("编辑员工") self.ui.edit_name.setText(emp_data["name"]) # 其他字段回填...逻辑说明:弹窗复用同一个表单。传入emp_data就是编辑模式,不传就是新增模式,保存按钮的槽函数里按这个标记决定调用 insert 还是 update。前端校验在公司名、手机号等字段上用QRegularExpressionValidator限制输入格式,比后端报错再弹 MessageBox 友好很多。
参数说明:emp_data是从 QTableWidget 当前行取出的字典,因为每行单元格都塞了Qt.UserRole数据,所以能拿到表格里看不到的dept_id和entry_date。保存时把整个表单的字段打包成字典传给 service 层,由 service 统一处理参数化插入。
4. MySQL 数据层设计:事务、连接池与统计报表的实现
4.1 表结构与外键约束:先拆人事和部门
数据层是整个系统的地基,建表时把员工表和部门表拆开是很关键的决定。如果员工表里直接存部门名称,后面改部门名称要把所有员工的记录都 update 一遍;拆开后员工表只存dept_id,改部门名只动部门表一条记录。
常用建表语句如下:
CREATE TABLE dept ( dept_id INT AUTO_INCREMENT PRIMARY KEY, dept_name VARCHAR(50) NOT NULL UNIQUE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE employee ( emp_id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL, gender TINYINT DEFAULT 1, phone VARCHAR(20), dept_id INT, entry_date DATE, salary DECIMAL(10,2), FOREIGN KEY (dept_id) REFERENCES dept(dept_id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:外键用ON DELETE SET NULL而不是CASCADE,是为了避免误删部门时把整个部门员工记录连带删掉。人事系统里误操作删数据是高频事故,保留员工记录、把部门置空更符合实际业务习惯。salary用 DECIMAL 而不是 FLOAT,是防止浮点误差影响工资计算。
参数说明:表名和字段用单数形式,关联查询时用JOIN dept取部门名称;核心表都指定 InnoDB 引擎,保证事务和外键约束生效。字符集用 utf8mb4,如果只支持中文和英文,utf8 也够,但 utf8mb4 兼容 emoji 输入,需求方偶尔会在备注里填表情。
4.2 连接池:为什么不能每次查询都新建连接
PyQt5 程序里最容易踩的坑是频繁 connect/disconnect MySQL。桌面端的查询频率远高于 Web 端,每次操作都走 TCP 握手、认证、断开,界面会出现明显卡顿,尤其是在工资统计这类需要遍历大量记录的页面。
这里采用连接池方案,用一个对象维护若干长连接,调用方请求时借一个,用完归还:
import pymysql from dbutils.pooled_db import PooledDB pool = PooledDB( creator=pymysql, maxconnections=8, mincached=2, maxcached=5, blocking=True, host="localhost", user="hr_system", password="your_password", database="hr_db", charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor )逻辑说明:PooledDB维护一个连接池,mincached=2表示启动时预创建两个连接,maxcached=5是池里最多闲置五个,maxconnections=8是并发上限,超出时blocking=True会让请求排队而不是报错。每次业务层查询用with pool.connection() as conn拿连接,查询完自动归还,避免连接被误关。
参数说明:cursorclass指定 DictCursor,返回的每行是字典,代码里通过emp["name"]访问字段,比元组下标可读性强很多。密码不建议写在代码里,项目里放在同级目录的.ini配置文件中,程序启动时用configparser读取。
4.3 部门统计与事务处理:一个报表走完 Commit/Rollback
人事系统另一个实用功能是部门人数与平均工资统计。常见做法是直接写聚合 SQL:
SELECT d.dept_name, COUNT(e.emp_id) AS emp_count, COALESCE(ROUND(AVG(e.salary), 2), 0.00) AS avg_salary FROM dept d LEFT JOIN employee e ON d.dept_id = e.dept_id GROUP BY d.dept_id, d.dept_name ORDER BY emp_count DESC;逻辑说明:用 LEFT JOIN 是为了把空部门也统计进去,如果用 INNER JOIN,没人的部门根本不会出现在结果里,需求方看报表时会觉得数据丢了。COALESCE把 AVG 可能返回的 NULL 转成 0,避免前端展示 “None”。
如果员工导入功能涉及批量插入,事务就派上用场了。把多条 insert 包在同一个事务里,任何一条失败都回滚,避免插到一半留下脏数据:
def import_employees(self, data_list): conn = pool.connection() try: with conn.cursor() as cursor: for item in data_list: cursor.execute( "INSERT INTO employee (name, gender, phone, dept_id) " "VALUES (%s, %s, %s, %s)", (item["name"], item["gender"], item["phone"], item["dept_id"]) ) conn.commit() except Exception as e: conn.rollback() raise e finally: conn.close()5. PyQt5 + MySQL 避坑指南:从连不上库到界面卡死
5.1 连接 MySQL 报错 “Access denied for user”
现象:程序启动时用户名密码都对,却始终报Access denied。原因:MySQL 8.0 默认使用caching_sha2_password认证插件,而 PyMySQL 0.9 以下版本只支持mysql_native_password。解决:升级 PyMySQL 到 1.0+,或执行ALTER USER 'hr_system'@'localhost' IDENTIFIED WITH mysql_native_password BY '...';切换认证方式。项目交付后对方环境不确定时,优先把 PyMySQL 版本写进 requirements.txt。
5.2 QTableWidget 修改单元格后数据没写库
现象:表格里双击改了手机号,点保存后数据库没变化。原因:用户关闭程序时表格内容根本没回写。解决:保存按钮的槽函数里遍历表格所有行,从单元格QTableWidgetItem取值,组装成字典传给update_employee(),不要指望表格自己知道数据变了。更稳妥的是封装一个collect_table_data()方法,专门从 QTableWidget 读值,和刷新表格的逻辑对称。
5.3 界面长时间无响应,拖动窗口像幻灯片
现象:点击“统计工资”后,程序界面卡住几分钟。原因:耗时查询跑在 Qt 主线程里,阻塞了事件循环。解决:把查询封装到QThread子线程或QThreadPool + QRunnable中,完成后通过信号把结果传回主线程刷新表格。即便查询只用 0.5 秒,在查询期间用户拖动窗口也会卡顿,体验很差。项目里我按 1 秒作为阈值:单表查询不超 1 秒走主线程,超 1 秒的统计报表一律丢给子线程。
5.4 数据库连接数爆满
现象:程序运行几个小时后,MySQL 报Too many connections。原因:某段代码获取连接后没归还,异常分支没走 finally。解决:所有取连接的地方统一用with pool.connection() as conn:语法,或者把释放操作放进 finally。另外,PooledDB 的maxconnections建议设置在 8~16 之间,太小不够用,太大会拖垮 MySQL 默认的 151 上限。
5.5 中文字符显示为问号
现象:界面和数据库表里所有中文都变成??。原因:表结构、连接池 charset、PyQt5 界面三处编码不统一。解决:建表时统一DEFAULT CHARSET=utf8mb4,连接池charset="utf8mb4",PyQt5 源文件顶部加# -*- coding: utf-8 -*-。注意 MySQL 的character_set_server也可能默认 latin1,连接后执行一次SET NAMES utf8mb4更保险。
6. 进阶:打包发布与一键数据备份
6.1 用 PyInstaller 打包成 exe 的配置要点
开发完 PyQt5 项目,交付时不可能让对方装 Python 环境。用 PyInstaller 打包是通用做法,但有几个坑直接影响能不能跑起来。先用命令生成 spec 文件:
pyinstaller --windowed --name HRSystem --icon=app.ico main.py生成后编辑同名.spec文件,重点改三处:datas把ui/目录和config.ini放进去;hiddenimports里加上pymysql和dbutils,否则打包出的 exe 运行时会报 No module named;excludes排除用不到的 Qt 模块,可以减小几十 MB 体积。打包完把config.ini和 exe 放在同一目录,运行时用os.path.dirname(sys.executable)拼接配置路径,别用相对路径。
6.2 用 mysqldump 做定时备份
桌面系统的数据全在本地 MySQL,如果机器出问题,所有员工档案都丢。项目里我在主窗口加了一个“备份数据”按钮,背后调用 mysqldump,把整个库导出成带日期的 SQL 文件存到备份目录:
mysqldump -u hr_system -p hr_db > backup_$(date +%Y%m%d_%H%M%S).sql如果在 Windows 下跑,用 Python 的subprocess.run()调用同一条命令,参数换成字符串拼接。备份文件可以直接用 MySQL Workbench 导入,不用额外写恢复逻辑。如果需求方有服务器,把这个命令丢进计划任务里每周跑一次更省心。
6.3 给记录表做归档:避免数据量拖慢列表
员工离职后记录还在表里,但查询列表时需求方通常只想看在职的。我一般会在 employee 表加一个status字段,0 表示在职、1 表示离职,刷新表格时默认过滤 status=0。这个方案比物理删除更安全,误操作后后期能救。员工量达到几万条后再考虑按月分区,中小系统一个索引就能扛住。
从那以后,我每次交付这类 PyQt5 + MySQL 的桌面系统,都会强制走一遍备份命令、确认打包后的 exe 在另一台干净机器上能打开、再用空数据库执行一遍全流程操作。这套简易人事管理系统虽然功能不复杂,但登录、增删改查、统计、备份的完整链路都在,照着路径改造会比从零开始省不少时间。希望帮到你。
本文还有配套的精品资源,点击获取