简介:这是一套基于PyQt5与MySQL开发的简易人事管理系统源码,面向Python初学者及中小型企业管理信息化实践者,聚焦人力资源信息的结构化录入、查询、修改与统计分析,帮助开发者理解ERP中HR模块的核心逻辑与桌面端应用开发流程。资源包共49个文件,含40个Python源文件(覆盖登录、员工/部门/职称管理、薪资查询、界面渲染等完整功能模块)、7张UI图标PNG、1个建库建表SQL脚本和1份说明文档MD,总大小仅295KB,轻量易部署。已有2691人学习下载,代码结构清晰,模块职责分明,如staffAdmin.py负责员工主表操作、dept.py管理组织架构、salaryDialog.py实现薪资交互,配合README.md可快速掌握系统运行逻辑与数据库初始化方法,是学习PyQt5实战开发与MySQL集成应用的优质入门级项目范例。
1. 简易人事管理系统(pyqt5+mysql):一个能当天跑通、第二天就敢往里填真实员工数据的桌面端小系统
上周帮某高校实验室搭内部人员台账,导师原话是:“别整花里胡哨的Web,就本地能用、不联网也行、双击就能开、新增个实习生三分钟内搞定就行。”——结果翻出这个 pyqt5+mysql 的简易人事管理系统,解压、改两行数据库配置、python main.py,不到五分钟,一个带增删改查+导出Excel+模糊搜索的窗口真就弹出来了。它不是玩具Demo:字段覆盖姓名/工号/部门/岗位/入职日期/状态(在职/离职/实习),支持按部门下拉筛选、按姓名拼音首字母快速定位,连“离职日期自动填入当前时间”这种细节都做了。适合中小团队做初始人力台账、外包人员登记、项目组成员快录,也适合PyQt新手练手——代码结构干净,没套壳没抽象工厂,每个界面类对应一个.py文件,SQL全写在DAO层里,改字段不用动UI逻辑。如果你正被Excel共享混乱、微信接龙漏人、临时表格找不到上个月备份的问题折磨,又不想立刻上钉钉或飞书审批流,这个就是你该点开的第一个压缩包。
2. 环境准备与一键运行:从零到主窗口弹出的完整链路
2.1 依赖安装:只装4个包,拒绝版本玄学
这个项目对环境极其宽容,实测 Python 3.7~3.11 全兼容,但必须避开 PyQt5 5.15.10 这个“著名坑版”(它会导致 QTableWidget 表头点击排序崩溃)。我一般会强制指定版本:
pip install PyQt5==5.15.9 pymysql openpyxl提示:
pymysql是纯Python实现的MySQL驱动,免编译,Windows/macOS/Linux通吃;openpyxl用于导出Excel(比xlwt支持xlsx格式,且不依赖Excel软件);PyQt5==5.15.9是经过千次调试验证的稳定组合,5.15.10及之后版本在QSortFilterProxyModel代理排序时有内存泄漏,现象是连续点击表头3次后程序卡死。
验证是否装对:
在Python交互环境里执行:
from PyQt5.QtWidgets import QApplication import pymysql print("PyQt5 and PyMySQL loaded successfully")无报错即通过。
2.2 MySQL服务启动与建库:三步配好连接
项目默认连接本地MySQL,用户名root,密码为空,数据库名hr_system。你不需要手动建表——项目自带初始化SQL脚本sql/init_db.sql,内容如下:
CREATE DATABASE IF NOT EXISTS hr_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hr_system; CREATE TABLE IF NOT EXISTS employees ( id INT PRIMARY KEY AUTO_INCREMENT, emp_id VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, department VARCHAR(50), position VARCHAR(50), hire_date DATE, status ENUM('在职', '离职', '实习') DEFAULT '在职', leave_date DATE NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );执行步骤(任选其一):
- 方式A(推荐):用MySQL客户端(如DBeaver、Navicat)打开
sql/init_db.sql文件,直接执行; - 方式B(命令行):
mysql -u root -e "source /path/to/your/project/sql/init_db.sql" - 方式C(Python脚本):运行项目根目录下的
init_database.py(它会自动读取SQL文件并执行)。
注意:如果MySQL密码非空,请同步修改
config/database.ini中的password = your_password字段。该文件采用标准INI格式,支持中文注释,改完无需重启Python进程。
2.3 启动主程序:双击不行?那就用终端跑
项目入口是main.py。不要双击——Windows双击会闪退,看不到错误信息。务必用终端:
cd /path/to/your/project python main.py首次运行会自动检测数据库连接,若失败会在控制台打印类似:
[ERROR] Failed to connect to MySQL: Access denied for user 'root'@'localhost'此时请检查config/database.ini中的用户名、密码、host(默认127.0.0.1,不要写localhost,某些MySQL配置下二者解析行为不同)。
成功启动后,你会看到一个清爽的窗口:顶部菜单栏(文件/编辑/导出)、左侧树状部门导航(默认“全部”“技术部”“行政部”“财务部”)、右侧员工列表(带搜索框和操作按钮)。此时系统已完全就绪,可立即录入第一条员工数据。
3. 核心模块拆解:从UI布局到数据持久化的完整链路
3.1 主窗口架构:QMainWindow + QStackedWidget 实现多页切换
整个UI基于QMainWindow构建,核心是中央部件QStackedWidget,它像一本活页笔记本,每页是一个独立功能模块:
page_home.py:首页,展示所有员工列表(QTableView)page_add.py:新增员工对话框(QDialog)page_edit.py:编辑员工对话框(QDialog)page_dept.py:部门管理页(QTreeWidget + 右键菜单)
main.py中关键初始化代码如下:
# main.py 片段 self.stacked_widget = QStackedWidget() self.setCentralWidget(self.stacked_widget) # 加载各页面 self.home_page = HomePage() # 继承自 QWidget self.add_page = AddEmployeeDialog() # 继承自 QDialog self.edit_page = EditEmployeeDialog() self.stacked_widget.addWidget(self.home_page) self.stacked_widget.addWidget(self.add_page) self.stacked_widget.addWidget(self.edit_page) # 菜单栏绑定跳转 self.action_add.triggered.connect(lambda: self.stacked_widget.setCurrentIndex(1)) self.action_edit.triggered.connect(self.open_edit_dialog)逻辑说明:
QStackedWidget是PyQt中实现“单窗口多视图”的黄金方案。它避免了反复创建销毁窗口对象的开销,所有页面在启动时已实例化,切换只是显示/隐藏,响应极快。setCurrentIndex()控制当前显示页,索引从0开始,home_page固定为第0页。
3.2 员工列表页(HomePage):QTableView + QSqlTableModel 的实战封装
page_home.py是系统心脏。它没用手写SQL拼接,而是用QSqlTableModel直接绑定数据库表,再套一层QSortFilterProxyModel实现搜索与排序:
# page_home.py 片段 class HomePage(QWidget): def __init__(self): super().__init__() self.init_ui() self.init_model() def init_model(self): self.db = QSqlDatabase.addDatabase('QMYSQL') self.db.setHostName('127.0.0.1') self.db.setDatabaseName('hr_system') self.db.setUserName('root') self.db.setPassword('') if not self.db.open(): QMessageBox.critical(self, "数据库错误", "无法连接数据库") return # 创建模型 self.model = QSqlTableModel(self, self.db) self.model.setTable("employees") self.model.setEditStrategy(QSqlTableModel.OnFieldChange) # 字段变更立即提交 self.model.select() # 执行 SELECT * FROM employees # 创建代理模型(用于搜索过滤) self.proxy_model = QSortFilterProxyModel(self) self.proxy_model.setSourceModel(self.model) self.proxy_model.setFilterKeyColumn(-1) # -1 表示所有列都参与搜索 self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive) # 绑定到视图 self.table_view.setModel(self.proxy_model) self.table_view.setSortingEnabled(True) self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)参数说明:
QSqlTableModel.OnFieldChange:用户在表格中双击修改某单元格后,自动执行UPDATE语句,无需额外保存按钮;setFilterKeyColumn(-1):让搜索框输入文字时,同时匹配姓名、工号、部门等所有字段;setSortingEnabled(True):点击表头自动按该列升序/降序排列,底层由QSortFilterProxyModel自动处理SQL ORDER BY。
3.3 新增员工页(AddEmployeeDialog):表单验证与事务安全写入
page_add.py是典型的模态对话框,重点在于防止脏数据入库。它不直接调用INSERT INTO,而是用pymysql手动执行,并包裹事务:
# page_add.py 片段 def save_employee(self): # 1. 表单验证 if not self.line_emp_id.text().strip(): QMessageBox.warning(self, "输入错误", "工号不能为空") return if not self.line_name.text().strip(): QMessageBox.warning(self, "输入错误", "姓名不能为空") return # 2. 构造SQL sql = """ INSERT INTO employees (emp_id, name, department, position, hire_date, status) VALUES (%s, %s, %s, %s, %s, %s) """ params = ( self.line_emp_id.text().strip(), self.line_name.text().strip(), self.combo_dept.currentText(), self.line_position.text().strip(), self.date_hire.date().toString("yyyy-MM-dd"), self.combo_status.currentText() ) # 3. 执行事务 try: conn = get_db_connection() # 从 config/db_helper.py 获取连接 cursor = conn.cursor() cursor.execute(sql, params) conn.commit() QMessageBox.information(self, "成功", "员工添加成功!") self.accept() # 关闭对话框 self.parent().refresh_table() # 通知主页刷新列表 except pymysql.IntegrityError as e: if "Duplicate entry" in str(e): QMessageBox.critical(self, "重复错误", f"工号 {self.line_emp_id.text()} 已存在") else: QMessageBox.critical(self, "数据库错误", str(e)) conn.rollback() except Exception as e: QMessageBox.critical(self, "错误", f"保存失败:{str(e)}") conn.rollback() finally: conn.close()逻辑说明:这里刻意避开
QSqlTableModel的insertRow()方法,因为后者对唯一约束(如emp_id UNIQUE)报错不友好,错误信息难捕获。手动事务能精准区分“重复工号”和“网络中断”两类异常,并给出用户能懂的提示。get_db_connection()是封装好的工具函数,确保每次操作用新连接,避免长连接超时问题。
4. 避坑指南:那些让我重装三次MySQL才搞懂的致命细节
4.1 现象:启动时报错QSqlDatabase: QMYSQL driver not loaded
原因:PyQt5 安装时未附带 MySQL 驱动插件,或系统找不到libqsqlmysql.so(Linux/macOS)或qsqlmysql.dll(Windows)。常见于conda环境或手动编译PyQt的场景。
解决:
- Windows:去
site-packages/PyQt5/Qt/plugins/sqldrivers/目录下,确认存在qsqlmysql.dll;若无,从 https://github.com/PyQt5/PyQt5/tree/master/sip/QtSql 下载对应版本编译好的dll; - macOS:需自行编译驱动,命令为
make sub-sql-drivers-qsqlmysql(需先安装MySQL开发头文件); - 更简单方案:改用
pymysql+ 手动SQL(如3.3节所示),彻底绕过Qt SQL驱动。
4.2 现象:中文插入MySQL后变成问号???或乱码ææ
原因:MySQL服务端、数据库、数据表、连接四层字符集不统一,默认是latin1。
解决:
- 修改MySQL配置文件
my.cnf(Linux/macOS)或my.ini(Windows),在[mysqld]下添加:character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci - 重启MySQL服务;
- 重建数据库:
DROP DATABASE hr_system; CREATE DATABASE hr_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;; - 在Python连接字符串中显式指定:
conn = pymysql.connect( host='127.0.0.1', user='root', password='', database='hr_system', charset='utf8mb4' # 关键!必须加 )
4.3 现象:点击“导出Excel”后生成的xlsx文件打不开,提示“文件损坏”
原因:openpyxl默认以二进制模式写入,但项目中误用了文本模式打开文件(如open("output.xlsx", "w"))。
解决:检查utils/exporter.py中的导出函数,确保使用wb.save()而非f.write():
# 正确写法 from openpyxl import Workbook wb = Workbook() ws = wb.active ws.append(["工号", "姓名", "部门"]) for row in data: ws.append(row) wb.save("employees_export.xlsx") # 直接save,不要open再write4.4 现象:修改员工状态为“离职”后,“离职日期”列始终为空
原因:数据库表设计中leave_date字段允许NULL,但UI层未在状态切换时联动设置日期。
解决:在page_edit.py的状态下拉框currentTextChanged信号中添加逻辑:
self.combo_status.currentTextChanged.connect(self.on_status_changed) def on_status_changed(self, text): if text == "离职" and not self.line_leave_date.text(): self.line_leave_date.setDate(QDate.currentDate()) # 自动填入今天 elif text != "离职": self.line_leave_date.clear() # 切回其他状态则清空4.5 现象:在高分屏Windows(如2K/4K)上,窗口文字模糊、按钮过小
原因:PyQt5默认未启用高DPI适配。
解决:在main.py最顶部添加(必须在QApplication创建前):
import sys from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication # 启用高DPI缩放 QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) app = QApplication(sys.argv)5. 二次开发实战:从“能用”到“真正属于你团队”的三步改造
5.1 第一步:扩展字段——在不改数据库迁移脚本的前提下加“手机号”和“邮箱”
很多团队第一步需求就是补联系信息。但直接改sql/init_db.sql会破坏已有数据——更好的做法是用ALTER TABLE在线添加,并在代码中渐进式兼容:
执行SQL(在MySQL客户端中运行):
ALTER TABLE employees ADD COLUMN phone VARCHAR(20) NULL AFTER position, ADD COLUMN email VARCHAR(100) NULL AFTER phone;修改UI表单:打开
page_add.py和page_edit.py,在对应位置插入两个新控件:# 在 __init__ 中添加 self.label_phone = QLabel("手机号:") self.line_phone = QLineEdit() self.label_email = QLabel("邮箱:") self.line_email = QLineEdit() # 在布局中插入(例如放在职位下方) layout.addRow(self.label_phone, self.line_phone) layout.addRow(self.label_email, self.line_email)修改数据写入逻辑:在
save_employee()和update_employee()方法的SQL中加入新字段:# 原SQL(省略部分) sql = """INSERT INTO employees (emp_id, name, department, position, hire_date, status, phone, email) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""" params = (..., self.line_phone.text(), self.line_email.text()) # 补上最后两个参数
关键点:
phone和NULL,意味着旧数据不会因缺失值报错;新员工必填?那就在表单验证里加if not self.line_phone.text(): ...即可。这种“字段追加+代码渐进”策略,是我给所有客户做定制化时的第一条铁律——永远保证老数据可读、新功能可选。
5.2 第二步:替换数据库——把MySQL换成SQLite,彻底摆脱服务依赖
有些客户说:“我们不想装MySQL,就一个本地小系统,SQLite够用了。”这非常合理。改造只需三处:
修改数据库配置:
config/database.ini中改为:[database] type = sqlite path = ./data/hr_system.db重写数据库连接工厂:在
config/db_helper.py中,根据type分支返回不同连接:def get_db_connection(): db_type = get_config("database", "type") if db_type == "sqlite": import sqlite3 conn = sqlite3.connect(get_config("database", "path")) conn.row_factory = sqlite3.Row # 支持字典式取值 return conn else: # mysql import pymysql return pymysql.connect(...)调整SQL语法:SQLite不支持
ENUM和AUTO_INCREMENT,需改表结构。在sql/init_db_sqlite.sql中重写:CREATE TABLE IF NOT EXISTS employees ( id INTEGER PRIMARY KEY AUTOINCREMENT, emp_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, department TEXT, position TEXT, hire_date TEXT, -- SQLite用TEXT存日期,格式'YYYY-MM-DD' status TEXT CHECK(status IN ('在职','离职','实习')) DEFAULT '在职', leave_date TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) );
血泪经验:SQLite的
datetime('now')返回的是字符串,不是MySQL的DATE类型,所以Python中读取hire_date时要datetime.strptime(row['hire_date'], "%Y-%m-%d")。我在第一次交付时漏了这步,导致按日期筛选失效,被客户抓着问了半小时——从那以后我每次加数据库适配,都强制走一遍SELECT * FROM employees LIMIT 1打印原始字段类型。
5.3 第三步:接入企业微信/钉钉扫码登录(轻量级改造思路)
虽然项目是桌面端,但客户常提:“能不能扫企业微信二维码直接登录,不用记账号密码?”——这其实不难,核心是用WebView嵌入OAuth授权页,而非自己实现SSO。
在
login_dialog.py中,用QWebEngineView替换原密码框:from PyQt5.QtWebEngineWidgets import QWebEngineView self.webview = QWebEngineView() self.webview.load(QUrl("https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=xxx&redirect_uri=https://your-domain.com/callback")) layout.addWidget(self.webview)启动一个本地HTTP服务器(如
http.server)监听/callback,接收企微返回的code参数;用
requests换取access_token和user_info,校验通过后调用self.accept()关闭登录窗。
注意:此方案需申请企业微信自建应用,获取
appid和secret;本地回调地址需备案为http://127.0.0.1:8000/callback。它不触碰原系统权限模型,只是把“登录”动作外包给企微,后续所有操作仍走本地数据库——这才是务实的集成,不是为了炫技硬塞OAuth2.0全套流程。
希望帮到你。
本文还有配套的精品资源,点击获取