ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

PyQt5+MySQL简易人事系统:本地桌面端员工台账快速搭建

PyQt5+MySQL简易人事系统:本地桌面端员工台账快速搭建

简介:这是一套基于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。
解决:

  1. 修改MySQL配置文件my.cnf(Linux/macOS)或my.ini(Windows),在[mysqld]下添加:
    character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
  2. 重启MySQL服务;
  3. 重建数据库:DROP DATABASE hr_system; CREATE DATABASE hr_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;;
  4. 在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再write

4.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在线添加,并在代码中渐进式兼容:

  1. 执行SQL(在MySQL客户端中运行):

    ALTER TABLE employees ADD COLUMN phone VARCHAR(20) NULL AFTER position, ADD COLUMN email VARCHAR(100) NULL AFTER phone;
  2. 修改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)
  3. 修改数据写入逻辑:在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和email字段设为NULL,意味着旧数据不会因缺失值报错;新员工必填?那就在表单验证里加if not self.line_phone.text(): ...即可。这种“字段追加+代码渐进”策略,是我给所有客户做定制化时的第一条铁律——永远保证老数据可读、新功能可选。

5.2 第二步:替换数据库——把MySQL换成SQLite,彻底摆脱服务依赖

有些客户说:“我们不想装MySQL,就一个本地小系统,SQLite够用了。”这非常合理。改造只需三处:

  1. 修改数据库配置:config/database.ini中改为:

    [database] type = sqlite path = ./data/hr_system.db
  2. 重写数据库连接工厂:在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(...)
  3. 调整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。

  1. 在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)
  2. 启动一个本地HTTP服务器(如http.server)监听/callback,接收企微返回的code参数;

  3. 用requests换取access_token和user_info,校验通过后调用self.accept()关闭登录窗。

注意:此方案需申请企业微信自建应用,获取appid和secret;本地回调地址需备案为http://127.0.0.1:8000/callback。它不触碰原系统权限模型,只是把“登录”动作外包给企微,后续所有操作仍走本地数据库——这才是务实的集成,不是为了炫技硬塞OAuth2.0全套流程。

希望帮到你。

本文还有配套的精品资源,点击获取

返回列表