PyQt5+MySQL人事管理系统开发:从表结构到打包避坑

发布时间:2026/10/13 15:42:58
PyQt5+MySQL人事管理系统开发:从表结构到打包避坑 简介简易人事管理系统基于PyQt5MySQL开发属于ERP人力资源管理模块面向中小企业HR或相关专业学习者用于实现员工、部门、薪资、考勤等信息的集成化管理与可视化操作。资源压缩包共49个文件、约295KB包含40个Python源码文件、7张界面效果图、1个SQL数据库脚本和1个说明文档py文件按员工管理、部门管理、职称管理、薪资台账、登录鉴权等模块划分SQL脚本可快速初始化数据库表结构。资源已有2688人学习下载读者从中可获取一套可直接运行的完整系统源码包含登录、增删改查员工/部门/职称/管理员、薪资查询与历史台账、图片资源封装等常用功能实现适合作为课程设计、毕业设计或PyQt5MySQL初学者的对照练习项目。目录结构清晰便于按模块阅读和二次开发。1. 简易人事管理系统PyQt5 加 MySQL 的桌面端最小闭环PyQt5 配 MySQL 做一套简易人事管理系统是桌面开发入门里最完整的闭环练习。它不像网页系统要分前后端部署也不像 Excel 台账越堆越乱——员工档案、部门信息、入离职状态都落在本地 GUI 里表单录入、表格查询、按钮操作就能完成日常维护。常见的落地组合是PyQt5 画界面、MySQL 存数据、PyMySQL 做连接。适合想要一个能打包分发的内部工具的小团队也适合想系统练一遍 GUI 加数据库技能的开发者。下面按表结构、连接层、界面、踩坑、交付的顺序展开全程都是可复制的最小代码。2. 从建表开始人事系统的表结构怎么设计才不返工2.1 为什么选 MySQL 而不是 SQLite做桌面人事系统很多人第一反应是 SQLite零配置、单文件、Python 标准库自带支持听起来最省事。但标题既然定了 MySQL说明你面对的场景大概率不是“一个人在自己电脑上记台账”而是“几个人在同一台服务器或局域网里共用一套数据”。SQLite 的并发写是全局锁两个人同时录入员工信息时经常报 database is lockedMySQL 作为服务端数据库天然支持多客户端并发数据统一落在服务器上每台电脑只要装客户端界面连过去就行。另一个现实理由是备份和迁移。MySQL 有 mysqldump 可以定时导出、异地存放SQLite 如果没人主动拷贝一个误操作删掉文件就全没了。人事数据属于删了就没法恢复的敏感内容给数据留“后悔药”的优先级很高。我的建议很简单单机自用可以选 SQLite但只要涉及多台机器、多人维护或数据要长期保留直接上 MySQL别给自己留返工的机会。2.2 人事系统的最小表结构员工主表加部门表表结构是这套系统里返工成本最高的部分界面写完了再改表所有 SQL 和表单都要跟着动。我的经验是最小可用设计只需要两张表department 和 employee。department 存部门名称和负责人employee 存员工档案通过 dept_id 外键关联。这样部门改名只改一条记录不会在员工表里留下 N 处脏数据。employee 表几个关键字段的设计理由如下字段类型说明emp_noVARCHAR(20) UNIQUE工号业务唯一标识界面增删改查都以它为准nameVARCHAR(30) NOT NULL姓名必填项genderENUM(男,女)简单场景下枚举足够用dept_idINT部门外键关联 department 表phoneVARCHAR(20)电话用字符串存避免前导零丢失hire_dateDATE入职日期统一 YYYY-MM-DDsalaryDECIMAL(10,2)薪资固定两位小数不用浮点statusENUM(在职,离职)软删除标记离职不删数据两个字段要特别强调。emp_no 必须 UNIQUE员工重名很常见但工号不会重所有增删改查都以 emp_no 定位数据。salary 用 DECIMAL(10,2) 而不是 FLOAT浮点数的二进制存储会让 0.1 加 0.2 出现尾差薪资数据出现小数点误差就是事故。status 字段做软删除——离职员工不是从库删掉而是标记状态历史数据留着后面查考勤、查薪资记录都有依据。2.3 建表 SQL 和初始化数据下面是完整的建库建表脚本直接拷到 MySQL 客户端执行即可CREATE DATABASE IF NOT EXISTS hr_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hr_system; CREATE TABLE department ( dept_id INT AUTO_INCREMENT PRIMARY KEY, dept_name VARCHAR(50) NOT NULL UNIQUE, manager VARCHAR(30) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE employee ( emp_id INT AUTO_INCREMENT PRIMARY KEY, emp_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(30) NOT NULL, gender ENUM(男,女) DEFAULT 男, dept_id INT, position VARCHAR(30), phone VARCHAR(20), email VARCHAR(50), hire_date DATE, salary DECIMAL(10,2), status ENUM(在职,离职) DEFAULT 在职, FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO department (dept_name, manager) VALUES (技术部, 某主管), (人事部, 某主管); INSERT INTO employee (emp_no, name, gender, dept_id, position, phone, hire_date, salary, status) VALUES (E001, 员工A, 男, 1, 开发工程师, 13800000001, 2023-03-15, 12000.00, 在职), (E002, 员工B, 女, 2, 招聘专员, 13800000002, 2022-07-01, 9000.00, 在职);这里的字符集参数一个都不能省。数据库、表、连接三个层面的字符集如果不一致中文写到一半就会变成问号而且已经写坏的字符很难恢复。utf8mb4 是 utf8 的超集能存表情符号和生僻字人事备注里偶尔会出现这些直接用 utf8mb4 是零成本的选择。外键建议保留尽管 PyQt5 代码里不一定用得上它至少能在 SQL 层面阻止你插入不存在的 dept_id相当于给数据完整性上了一道保险。注意如果公司有现成的工号规则emp_no 不要用自增 ID 冒充。自增 ID 在数据迁移时很容易变化工号是打印在工牌上的一旦和系统对不上人事那边会天天来找你。emp_no 用业务规则生成比如 E001、E002 顺序递增或按“部门前缀加序号”拼。3. 打通 MySQL 连接层PyMySQL 封装与 CRUD 最小实现3.1 PyMySQL 还是 mysql-connector-pythonPython 连 MySQL 有两类驱动一类是官方维护的 mysql-connector-python一类是社区常用的 PyMySQL。前者功能全但包比较重安装时会带上不少依赖后者是纯 Python 实现pip 安装零编译几乎所有环境都能跑。做简易人事系统这种规模的应用两者功能没有本质差别我一般选 PyMySQL理由只有一个装机成本低打包分发时不用处理底层库的兼容问题。安装命令就一条pip install pyqt5 pymysqlPyQt5 安装包比较大下载慢是正常的建议配国内镜像源加速。装完用python -c import PyQt5, pymysql; print(ok)验证能打印 ok 说明环境正常后面报错就可以排除依赖缺失这一层。3.2 桌面程序的连接管理单连接加自动重连Web 后端喜欢用连接池因为要应对大量短请求桌面程序只有一个客户端、一个用户操作用连接池纯属多余。我的做法是启动时建一个长连接每次执行 SQL 前先 ping 一下断了就重连。核心是一个 DB 封装类把连接、查询、执行统一收口import pymysql class DB: def __init__(self, config): self.config config # dict: host/user/password/database self.conn None def connect(self): self.conn pymysql.connect( hostself.config[host], userself.config[user], passwordself.config[password], databaseself.config[database], charsetutf8mb4, # 与建表字符集保持一致 cursorclasspymysql.cursors.DictCursor, # 返回字典按字段名取值 autocommitTrue # 每条 SQL 自动提交 ) return self.conn def ping(self): try: self.conn.ping(reconnectTrue) # 底层自动重连 except Exception: self.connect() # ping 失败就重建连接 def query(self, sql, paramsNone): self.ping() with self.conn.cursor() as cur: cur.execute(sql, params) return cur.fetchall() def execute(self, sql, paramsNone): self.ping() with self.conn.cursor() as cur: cur.execute(sql, params) return cur.rowcount这段代码里有三个参数是踩过坑之后才加上的。charset 不指定默认可能是 latin1中文直接乱码。cursorclass 用 DictCursor 后查询结果是一条条字典写代码时row[name]比row[2]可读性强得多。autocommit 设为 True 意味着不用手动 commit适合人事系统这种单条操作语义清晰的场景如果后面要做批量导入、需要事务回滚再改成手动 commit。注意MySQL 服务端有个 wait_timeout 参数连接空闲超过阈值服务端会主动断开但客户端不知道。加了 ping(reconnectTrue) 后每次执行前都检查断了自动重连报错 “MySQL server has gone away” 就再也见不到。ping 抛出异常时要重建连接不能只依赖 reconnectTrue连接完全失效时它不保证能恢复。3.3 CRUD 最小实现查询、新增、修改、删除有了 DB 封装业务层就是几个函数。建议把这块独立放一个模块和界面代码分开后面维护时不用翻一整页控件代码。核心实现如下def list_employees(db, keyword): sql SELECT e.emp_no, e.name, e.gender, d.dept_name, e.position, e.phone, e.hire_date, e.salary, e.status FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id WHERE e.status 在职 AND (e.name LIKE %s OR e.emp_no LIKE %s) ORDER BY e.emp_no like f%{keyword}% return db.query(sql, (like, like)) def add_employee(db, data): sql INSERT INTO employee (emp_no, name, gender, dept_id, position, phone, email, hire_date, salary, status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) return db.execute(sql, ( data[emp_no], data[name], data[gender], data[dept_id], data[position], data[phone], data[email], data[hire_date], data[salary], data[status] )) def update_employee(db, emp_no, data): sql UPDATE employee SET name%s, gender%s, dept_id%s, position%s, phone%s, email%s, hire_date%s, salary%s, status%s WHERE emp_no%s return db.execute(sql, ( data[name], data[gender], data[dept_id], data[position], data[phone], data[email], data[hire_date], data[salary], data[status], emp_no )) def delete_employee(db, emp_no): sql DELETE FROM employee WHERE emp_no%s return db.execute(sql, (emp_no,))list_employees 里 LEFT JOIN 保证员工即使还没分部门也能查出来LIKE 查询把关键字两头加 %配合 dept_name 可以做到“搜部门也能找到人”。hire_date 直接传 YYYY-MM-DD 格式的字符串MySQL 的 DATE 类型能自动转换。关于日期格式第 5 章还会专门讲。注意delete_employee 是真删除。前面表设计里提过 status 字段做软删除界面上的“删除”按钮默认应该走UPDATE employee SET status离职而不是 DELETE。保留这个函数是给管理员做数据清理用的界面别随便暴露。3.4 参数化查询写 SQL 的时候别用 f-string 拼参数很多初学者习惯这么写# 错误示范SQL 注入风险 引号转义地狱 sql fSELECT * FROM employee WHERE name {keyword} db.query(sql)员工叫“OBrien”这种带单引号的名字或者搜索框里输入一个 OR 11 --这条 SQL 要么直接报错要么把整张表拖出来。参数化查询的 %s 占位符不是简单的字符串替换而是驱动层面的预处理特殊字符会被安全转义。上面所有代码都用元组传参这个习惯从第一行 SQL 开始就要养成。另一个隐藏好处是代码可读性。SQL 模板和参数分离后SQL 里不用再拼一长串引号和加号出问题也好排查——参数就在元组里print 出来就能看。4. 把界面搭起来PyQt5 主窗口、表格与表单的落地写法4.1 Qt Designer 还是纯代码布局PyQt5 做界面有两条路线用 Qt Designer 拖控件生成 .ui 文件再加载或者纯代码写控件和布局。我推荐后者做这种简易人事管理系统原因很直接纯代码里所有控件都在同一个文件里可见改间距、加按钮直接搜索定位而 .ui 文件是 XML加载后调试时看不到控件层级改完还要重新编译无形中多一道工序。人事系统的界面复杂程度有限不外乎一个主窗口、一个表单对话框、几个按钮纯代码的可读性完全够用。反过来如果做一整套带复杂报表、多 Tab 的管理后台Qt Designer 能省大量排版时间。两条路线不冲突按项目规模选。4.2 主窗口搜索栏加 QTableView 的核心布局主窗口的标准布局是顶部一条搜索栏中间一整块 QTableView 表格底部三个操作按钮。代码如下from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTableView, QMessageBox, QDialog, QComboBox, QFormLayout, QDialogButtonBox) from PyQt5.QtGui import QStandardItemModel, QStandardItem class MainWindow(QMainWindow): def __init__(self, db): super().__init__() self.db db self.setWindowTitle(简易人事管理系统) self.resize(1000, 650) self._build_ui() self.refresh_table() def _build_ui(self): central QWidget(self) self.setCentralWidget(central) layout QVBoxLayout(central) # 搜索栏 bar QHBoxLayout() self.search_edit QLineEdit() self.search_edit.setPlaceholderText(输入姓名、工号或部门搜索) self.search_btn QPushButton(搜索) self.search_btn.clicked.connect(self.refresh_table) bar.addWidget(self.search_edit) bar.addWidget(self.search_btn) layout.addLayout(bar) # 员工表格 self.table QTableView() self.model QStandardItemModel(self) self.model.setHorizontalHeaderLabels( [工号, 姓名, 性别, 部门, 职位, 电话, 入职日期, 薪资, 状态] ) self.table.setModel(self.model) self.table.setSelectionBehavior(QTableView.SelectRows) # 整行选中 self.table.setEditTriggers(QTableView.NoEditTriggers) # 禁止表格内直接编辑 layout.addWidget(self.table, 1) # 操作按钮 btn_bar QHBoxLayout() add_btn QPushButton(新增员工) edit_btn QPushButton(编辑) quit_btn QPushButton(离职/删除) add_btn.clicked.connect(self.add_employee) edit_btn.clicked.connect(self.edit_employee) quit_btn.clicked.connect(self.quit_employee) for b in (add_btn, edit_btn, quit_btn): btn_bar.addWidget(b) layout.addLayout(btn_bar) def refresh_table(self): keyword self.search_edit.text().strip() rows list_employees(self.db, keyword) self.model.removeRows(0, self.model.rowCount()) for r in rows: self.model.appendRow([QStandardItem(str(r[k])) for k in (emp_no, name, gender, dept_name, position, phone, hire_date, salary, status)])两个 set 方法值得多说一句。setSelectionBehavior(SelectRows) 让用户点击任意一列都选中整行编辑和离职按钮取当前行数据时不会出现“只选到一个单元格”的困惑。setEditTriggers(NoEditTriggers) 禁止直接在表格里改数据所有修改必须走编辑对话框这样校验逻辑统一收敛在对话框里不会有人在表格里填出非法日期。refresh_table 每次重新拉全量数据再整体刷 model几千行以内完全没压力。注意 appendRow 里把所有字段都转成了 str因为 QStandardItem 只接受字符串日期和数字不转会显示成空串或类型异常。4.3 新增和编辑一个表单对话框复用新增和编辑的区别只是“有没有默认值”所以用一个对话框类构造时传 data 参数区分。这样校验逻辑只维护一份不会出现新增能过、编辑不能过这种不一致class EmployeeDialog(QDialog): def __init__(self, parentNone, dataNone): super().__init__(parent) self.data data self.setWindowTitle(编辑员工 if data else 新增员工) form QFormLayout(self) self.emp_no QLineEdit() self.name QLineEdit() self.gender QComboBox() self.gender.addItems([男, 女]) self.dept_id QComboBox() self.position QLineEdit() self.phone QLineEdit() self.hire_date QLineEdit() self.hire_date.setPlaceholderText(YYYY-MM-DD) self.salary QLineEdit() form.addRow(工号, self.emp_no) form.addRow(姓名, self.name) form.addRow(性别, self.gender) form.addRow(部门, self.dept_id) form.addRow(职位, self.position) form.addRow(电话, self.phone) form.addRow(入职日期, self.hire_date) form.addRow(薪资, self.salary) btns QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) btns.accepted.connect(self._validate_and_accept) btns.rejected.connect(self.reject) form.addRow(btns) if data: self.emp_no.setText(data[emp_no]) self.name.setText(data[name]) self.gender.setCurrentText(data[gender]) self.position.setText(data[position]) self.phone.setText(data[phone]) self.hire_date.setText(str(data[hire_date])) self.salary.setText(str(data[salary])) self.emp_no.setReadOnly(True) # 工号建好后不允许改 def _validate_and_accept(self): if not self.name.text().strip(): QMessageBox.warning(self, 校验失败, 姓名不能为空) return try: from datetime import date date.fromisoformat(self.hire_date.text().strip()) except ValueError: QMessageBox.warning(self, 校验失败, 日期格式必须是 YYYY-MM-DD) return self.accept() def get_data(self): return { emp_no: self.emp_no.text().strip(), name: self.name.text().strip(), gender: self.gender.currentText(), dept_id: self.dept_id.currentData(), position: self.position.text().strip(), phone: self.phone.text().strip(), hire_date: self.hire_date.text().strip(), salary: float(self.salary.text() or 0), status: 在职, }这里有两个细节是真实使用中才会发现的。dept_id 是下拉框但显示的是部门名称入库的是 dept_id所以要用addItem(dept_name, dept_id)把两个值绑在一起get_data 里用 currentData() 取数值。工号在编辑模式下 setReadOnly(True)防止改资料时顺手改了工号导致历史关联全部失效——这是人事系统里最隐蔽的破坏性操作。校验放在对话框里而不是主窗口是刻意为之新增和编辑走同一个入口校验只维护一份。日期校验用 date.fromisoformat它只接受 YYYY-MM-DD 格式用户填 2024/1/1 或 2024年1月1日 都会被拦截错误提示直接告诉用户正确格式。4.4 增删改的交互闭环主窗口里三个按钮的槽函数把 UI 和数据库串起来def add_employee(self): dlg EmployeeDialog(self) if dlg.exec_() QDialog.Accepted: add_employee(self.db, dlg.get_data()) self.refresh_table() def edit_employee(self): row self.table.currentIndex().row() if row 0: QMessageBox.information(self, 提示, 请先在表格中选择一行) return emp_no self.model.item(row, 0).text() # 第 0 列是工号 rows self.db.query( SELECT * FROM employee WHERE emp_no%s, (emp_no,)) if rows: dlg EmployeeDialog(self, datarows[0]) if dlg.exec_() QDialog.Accepted: update_employee(self.db, emp_no, dlg.get_data()) self.refresh_table() def quit_employee(self): row self.table.currentIndex().row() if row 0: QMessageBox.information(self, 提示, 请先在表格中选择一行) return emp_no self.model.item(row, 0).text() if QMessageBox.question( self, 确认, 将员工标记为离职) QMessageBox.Yes: self.db.execute( UPDATE employee SET status离职 WHERE emp_no%s, (emp_no,)) self.refresh_table()这套闭环的要点是取当前行工号 → 按工号查详情或改状态 → 操作完统一 refresh_table。用工号而不是行号作为业务主键是因为刷新之后行号会变而工号永远不变。所有按钮都做了空选择保护没选中行时弹提示而不是抛异常这是桌面工具给人“能用”感觉的基本门槛。到这里最小可用的简易人事管理系统已经能跑通启动连库、表格展示、搜索过滤、新增编辑、离职标记。第 5 章把实际运行中最高频的 5 个翻车现场挨个说一遍。5. 避坑手册PyQt5 MySQL 最常见的 5 个翻车现场以下问题我在这套系统的开发和使用过程中都真实遇到过按“现象 → 原因 → 解决”写方便对照排查。5.1 数据库里的中文变成问号现象界面里录入中文姓名刷新后表格里显示一串问号或者用 MySQL 客户端查看时全是乱码。原因字符集在三个层面不一致。最常见的是建库用了默认 latin1或 PyMySQL 连接时没指定 charset再或者 SQL 文件本身保存成了 GBK 编码。任何一层不一致中文都会在写入时被静默转坏而且已经写坏的字符很难恢复——这才是最要命的。解决三层统一到 utf8mb4。建库时 CHARACTER SET utf8mb4连接时 charsetutf8mb4SQL 文件保存为 UTF-8 无 BOM。如果已经写坏先把表里数据导出备份然后ALTER TABLE employee CONVERT TO CHARACTER SET utf8mb4再重新导入干净数据。转换前必须备份数据量大时这条命令可能锁表选在非工作时间执行。5.2 MySQL server has gone away 报错现象程序启动后放着不动半小时或几个小时后点搜索直接抛 pymysql.err.OperationalError提示 server has gone away。原因MySQL 服务端的 wait_timeout 默认 8 小时但很多自建实例和托管数据库会把超时时间调短有些甚至只有 30 分钟。连接空闲超过阈值服务端主动断开客户端还拿着过期的连接对象第一次操作就炸。解决3.2 节的 ping(reconnectTrue) 就是标准解法。这是客户端行为也可以把服务端 wait_timeout 调大比如set global wait_timeout28800但调服务端会影响所有连接不如客户端每次操作前 ping 来得稳。有个细节ping 失败后必须重建连接不能只调 ping因为 reconnectTrue 在连接完全失效时不保证能恢复最稳妥是 catch 到异常就 connect() 一次。5.3 界面卡死或崩溃在子线程里操作 UI现象数据量变大后你决定把查询丢到线程里执行结果程序要么直接崩溃要么界面变成“未响应”偶尔还弹出一堆看不清的线程报错。原因PyQt5 的 UI 只能在主线程里操作。子线程里调用 setText、appendRow 这类方法本质是跨线程访问对象Qt 的线程模型不保证安全轻则数据错乱重则段错误。加线程锁解决不了这个问题因为根因是 QWidget 和事件循环的线程亲和性。解决子线程做完查询后通过信号把结果发回主线程在主线程的槽函数里更新界面from PyQt5.QtCore import QThread, pyqtSignal class QueryThread(QThread): result_ready pyqtSignal(list) def __init__(self, db, keyword): super().__init__() self.db db self.keyword keyword def run(self): rows list_employees(self.db, self.keyword) self.result_ready.emit(rows)主窗口里把 result_ready 连接到刷新 model 的槽函数槽函数里只做 appendRow不碰数据库。这里还有个容易被忽略的点PyMySQL 的游标并不是严格线程安全的多个线程共用同一个连接存在风险。最稳妥的做法是每个线程用独立连接或者对人事系统这种低频操作干脆保持单线程——单线程加信号回传完全够用不必引入线程池。5.4 按日期筛选查不到数据现象界面里按入职日期筛选表格明明显示有 2024-05-01 入职的人但查出来是空的没有任何报错。原因hire_date 是 DATE 类型传入的字符串一旦不是严格的 YYYY-MM-DDMySQL 会把它转成 0000-00-00 或直接忽略条件。常见来源是 QDateEdit 的 toString 格式没指定Qt 默认可能输出“2024年05月01日”数据库当然对不上。解决所有日期进 SQL 之前统一转成 ISO 格式。如果界面用 QDateEdit取值时写self.date_edit.date().toString(yyyy-MM-dd)如果像 4.3 节那样用 QLineEdit 手输就在 get_data 里用 date.fromisoformat 校验并再格式化一次。查日期区间建议写hire_date BETWEEN %s AND %s两边都传 YYYY-MM-DD 字符串避免在 SQL 里用 TO_DATE 之类的函数保证不同 MySQL 版本下行为一致。5.5 表格数据多了以后滚动卡顿现象数据从几百条涨到上万条每次滚动表格都掉帧明显刷新一次要等好几秒。原因如果用的是 QTableWidget它是控件模式每个单元格都持有一个 QTableWidgetItem 对象一万条记录就是上万个控件实例内存和事件处理的开销全压在 GUI 线程上。逐行 setItem 填数据本质是创建对象再布局慢是必然的。解决换 QTableView QStandardItemModel这是 4.2 节已经在用的方案。QTableView 是视图数据在 model 里滚动时只渲染可见行性能不在一个量级。要更快就换成 QAbstractTableModel 自定义 model用重写的 data() 方法按需返回数据内存占用再降一档——但对简易人事系统QStandardItemModel 已经足够。配套习惯是批量更新时用self.model.setRowCount(0)而不是逐行 removeRows前者一次清空后者逐行触发信号数据量大时差距明显。真到几万行规模直接把查询改成分页LIMIT 加 OFFSET界面只展示当前页。6. 收尾交付打包、日志与数据备份的实战习惯系统能跑只是第一步真正能交给人事部门用的是另外三件事打包成可执行文件、留操作日志、定时备份数据。打包用 PyInstaller命令很简单pip install pyinstaller pyinstaller -w --onefile --name HRSystem main.py-w 去掉控制台窗口--onefile 打成单文件--name 指定名字。这里有两个坑一是 PyQt5 的插件和样式资源比较大第一次打出来的可执行文件可能上百 MB属正常现象别怀疑代码有鬼二是 main.py 里如果用相对路径加载配置文件或图标打包后工作目录会变建议所有外部资源用os.path.dirname(sys.argv[0])拼接绝对路径。PyMySQL 是纯 Python 库PyInstaller 一般能自动收集不用额外处理。日志是这套系统的后悔药。我见过太多“数据没了我也不知道怎么没的”的现场最小成本方案是给操作打点import logging logging.basicConfig( filenamehr_system.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) def log_op(action, emp_no, detail): logging.info(f{action} {emp_no} {detail})每条增删改都记一行什么时间、做了什么、改了哪个工号、改成了什么。出现数据争议时日志能快速定位是哪一次操作引起的。别嫌这写法土日志文件在关键时刻比数据库备份还值钱。数据备份方面我现在的习惯是每周自动导一次写入系统的计划任务mysqldump -u root -p**** hr_system backup_$(date %Y%m%d).sql同时每月把员工表导出 Excel 存档一是给人事做离线备份二是万一 MySQL 服务挂了Excel 能临时顶住日常查询。备份脚本跑完要检查生成文件的大小0 字节的备份等于没有备份这条是血泪经验。这套简易人事管理系统做到这里已经接近完整交付PyQt5 画界面PyMySQL 连 MySQL表结构可查可改操作有日志数据有备份。我最早一次给部门内部使用时就因为没做日志某天发现一条员工记录被改动查了半天也没查出是谁最后只能翻数据库日志非常被动。从那以后日志和备份成了我所有小型管理系统的默认配置哪怕程序只有 500 行也不省略。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询