简介:面向数据库课程设计的员工信息管理系统完整源码包,以Python与MySQL为核心实现,适合高校计算机相关专业学生进行课程设计参考、数据库实践入门或项目二次开发。压缩包共9个文件,约6.71MB,内含2个Python源码文件、2份Word课程设计报告、4张数据库设计图(E-R图、数据流图及系统运行界面)以及1个Markdown说明文档,文件类型覆盖代码、文档与图表,便于对照阅读和部署。系统涵盖员工信息的增删改查、部门与职位管理、账号管理等典型模块,详细演示了使用pymysql连接MySQL、创建数据表、执行SQL语句以及事务处理等关键操作;同时提供数据库表结构设计、E-R图与数据流图,能够帮助学习者快速梳理系统逻辑并撰写设计报告。已有4343人学习下载,项目从建库建表到业务逻辑实现形成完整闭环,目录结构清晰,可直接运行测试并在此基础上扩展功能,是课程设计答辩和Python与MySQL项目练手的实用素材。
1. 员工信息管理系统:一个基于 Python + MySQL 的数据库课程设计能做什么
期末周最常见的一种需求,就是做一个员工信息管理系统:Python 写逻辑,MySQL 存数据。这类课的考核点不在界面多华丽,而在能不能把增删改查、数据约束、多表关联、统计分析这四件事说清楚。这套源码恰好把这几件事都落地了——有登录鉴权、员工档案的完整增删改查、按部门和入职时间筛选、部门人数与平均薪资统计,数据落在 MySQL 里,Python 负责业务处理和界面交互。对正在做数据库课设的同学来说,它是可以直接跑通的参考实现;对刚学完 SQL 想看看 Python 怎么和数据库配合的人来说,它是一份非常完整的实操样例。花一个下午把它的代码逐行读一遍、改两个功能,比背两天 SQL 语法更有用。
2. 技术选型与代码结构:三大分层决定它是作业还是工程
2.1 为什么是 PyMySQL 加 tkinter,而不是更重的组合
这套项目的技术栈相当克制:Python 3 做业务逻辑,PyMySQL 做数据库驱动,tkinter 做桌面界面,MySQL 8.0 做存储。很多课程设计喜欢用 Flask 把系统包成 Web 应用,但桌面程序有一个好处——不用处理前后端联调、不用配端口和跨域,所有逻辑都在一个进程里跑,排查问题的时候链路短。
常见做法是直接用 mysql-connector-python 官方驱动,但 PyMySQL 在这里更合适。它体量小、装起来快,语法兼容 MySQL 协议,对课程设计这种数据量在几千行级别的系统完全够用。tkinter 是 Python 自带的标准库,不需要额外安装,放在课程设计里也方便答辩时现场演示。这套组合的代价是没有 ORM 映射,SQL 要自己写,但这恰恰是数据库课程设计想要的——老师要看到的就是你对 SQL 的掌握程度。
2.2 目录结构与调用关系:界面、业务、数据三层各司其职
拿到源码后先看目录结构,整个项目按三层来组织,这一点比功能本身更值得学习。
employee_system/ ├── app.py # 程序入口,初始化登录窗口 ├── db/ │ ├── db_config.py # 数据库连接参数配置 │ └── db_helper.py # 连接获取与释放的公共方法 ├── dao/ │ ├── employee_dao.py # 员工表数据访问层,封装所有 SQL │ ├── dept_dao.py # 部门表数据访问层 │ └── user_dao.py # 用户表数据访问层,登录用 ├── service/ │ ├── employee_service.py # 员工业务层:参数校验、统计计算 │ └── user_service.py # 登录业务层:密码校验 ├── ui/ │ ├── login_window.py # 登录窗口 │ └── main_window.py # 主窗口,功能菜单与表格展示 ├── sql/ │ └── init.sql # 建库建表语句 + 初始化数据 └── requirements.txt # 依赖清单我在看别人项目时第一眼就看 dao 层里有没有裸写 SQL。这套代码把 SQL 全部收在 dao 层,ui 层完全不出现 SQL 字符串,这是项目能二次开发的基础。ui 层调用 service,service 调 dao,dao 通过 db_helper 拿连接,数据流向是单向的,改界面不会影响 SQL,换数据库也不用动界面代码。
2.3 运行环境与依赖版本:先把地基踩实
requirements.txt 里的依赖很少,核心就是 PyMySQL 一个包,界面库 tkinter 随 Python 自带。如果用的是 Anaconda 发行版,tkinter 默认就有;如果是精简版 Python,需要单独安装 python-tk 包。MySQL 建议用 8.0 版本,因为 8.0 的默认认证插件是 caching_sha2_password,PyMySQL 在 1.0 版本以上已经兼容了这种认证方式,基本不会碰到老版本那种认证报错。
操作系统没有限制,Windows、macOS、Linux 都能跑。唯一要注意的是 MySQL 服务必须提前启动,而且 db_config.py 里的主机、端口、用户名、密码要和本机 MySQL 实际配置保持一致,这一步会在后面的避坑章节详细展开。
3. 数据库设计与初始化脚本:从建库到造数据的完整 DDL
3.1 三张表的字段设计:为什么这样建,每个约束解决什么问题
整套系统的核心是 sql/init.sql,它建了三张表:部门表、员工表、用户表。先看建库和部门表的语句。
CREATE DATABASE IF NOT EXISTS employee_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE employee_db; CREATE TABLE dept ( dept_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '部门ID', dept_name VARCHAR(50) NOT NULL UNIQUE COMMENT '部门名称', manager VARCHAR(30) COMMENT '部门负责人', phone VARCHAR(20) COMMENT '部门联系电话' ) ENGINE=InnoDB COMMENT='部门表';utf8mb4 是中文项目的硬性要求,如果不指定字符集,Windows 环境下 MySQL 默认的 latin1 会让所有中文变成问号。dept_name 加 UNIQUE 约束是为了保证部门不重名,后续员工表引用部门时,不会因为两个同名部门导致数据错乱。phone 字段不设 UNIQUE,因为空值在唯一索引里是可以重复的,硬加唯一约束反而会让多个空电话的部门插入失败。
员工表是最核心的表,字段多,约束也多。
CREATE TABLE employee ( emp_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '员工编号', emp_name VARCHAR(30) NOT NULL COMMENT '姓名', gender CHAR(2) NOT NULL DEFAULT '男' COMMENT '性别', birth_date DATE COMMENT '出生日期', phone VARCHAR(20) COMMENT '手机号', email VARCHAR(50) COMMENT '邮箱', hire_date DATE NOT NULL COMMENT '入职日期', dept_id INT NOT NULL COMMENT '所属部门ID', salary DECIMAL(10,2) DEFAULT 0.00 COMMENT '薪资', status TINYINT DEFAULT 1 COMMENT '在职状态:1在职 0离职', FOREIGN KEY (dept_id) REFERENCES dept(dept_id) ON DELETE RESTRICT, INDEX idx_hire_date (hire_date), INDEX idx_dept_status (dept_id, status) ) ENGINE=InnoDB COMMENT='员工表';gender 用 CHAR(2) 而不是枚举类型,是因为 MySQL 8.0 对 ENUM 的修改需要重建表,扩展性差;CHAR(2) 存“男”“女”两个汉字刚好在 utf8mb4 下占 2 个字符。hire_date 建索引,是因为系统里最常见的查询是按入职时间排查员工,索引能避免全表扫描。dept_id 加外键约束但 ON DELETE RESTRICT,意味着只要部门下还有员工,就不能直接删掉这个部门,这种约束在答辩现场非常加分。status 字段虽然简单,却是所有统计语句里最常用的条件,所以和 dept_id 组成了一个联合索引。
用户表负责登录,设计上把用户和员工分成两张表,逻辑上员工可以没有登录账号,账号也可以属于离职员工。
CREATE TABLE user ( user_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', username VARCHAR(30) NOT NULL UNIQUE COMMENT '登录名', password CHAR(64) NOT NULL COMMENT 'SHA256加密后的密码', emp_id INT COMMENT '关联员工ID', role VARCHAR(20) DEFAULT 'user' COMMENT '角色:admin/user', FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ON DELETE SET NULL ) ENGINE=InnoDB COMMENT='用户表';密码字段用 CHAR(64) 是因为 SHA256 摘要输出固定 64 个十六进制字符,不是用明文存储。role 字段区分管理员和普通用户,管理员可以看统计报表,普通用户只能维护自己的档案信息,这部分功能在 service 层里做判断。
3.2 初始化数据:造数据不是随便插,要能支撑后边的统计查询
光有表结构不够,统计功能需要一批有规律的数据才能演示出效果。初始化脚本里先插入部门,再插入员工,最后插入用户,顺序不能反,因为员工表引用了部门表的主键。
INSERT INTO dept (dept_name, manager, phone) VALUES ('技术部', '张伟', '010-12345678'), ('产品部', '李娜', '010-87654321'), ('市场部', '王强', '010-11223344'); INSERT INTO employee (emp_name, gender, birth_date, phone, email, hire_date, dept_id, salary, status) VALUES ('刘洋', '男', '1995-04-12', '13800001111', 'liuyang@example.com', '2020-03-15', 1, 15000.00, 1), ('陈静', '女', '1998-08-23', '13800002222', 'chenjing@example.com', '2021-07-01', 2, 12000.00, 1), ('赵磊', '男', '1992-11-30', '13800003333', 'zhaolei@example.com', '2018-01-10', 1, 18000.00, 1), ('孙悦', '女', '2000-02-14', '13800004444', 'sunyue@example.com', '2023-06-19', 3, 8000.00, 1), ('周涛', '男', '1996-09-09', '13800005555', 'zhoutao@example.com', '2022-05-23', 2, 10000.00, 0); INSERT INTO user (username, password, emp_id, role) VALUES ('admin', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918', 1, 'admin'), ('liuyang', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918', 1, 'user');注意两个细节:admin 用户和 liuyang 用户的密码哈希值一模一样,说明初始密码都设置成了同一个值,登录后可以修改。周涛那行数据的 status 是 0,表示离职员工,后面的统计查询里就会用到这个字段——统计在职人数时要用 WHERE status = 1 过滤掉他,这样统计结果才能和界面展示对应上。
3.3 验证建库成果:三条 SQL 看数据是否就位
初始化脚本执行完后,用三条查询验证一下。
-- 验证部门-员工关联 SELECT d.dept_name, COUNT(e.emp_id) AS emp_count FROM dept d LEFT JOIN employee e ON d.dept_id = e.dept_id AND e.status = 1 GROUP BY d.dept_id; -- 验证登录数据 SELECT username, role FROM user; -- 验证按入职年份分组 SELECT YEAR(hire_date) AS hire_year, COUNT(*) AS cnt FROM employee GROUP BY YEAR(hire_date) ORDER BY hire_year;第一条 SQL 里 LEFT JOIN 后跟 AND 条件而不是 WHERE,是为了把离职员工排除在统计之外,同时保留没有员工的部门显示为 0。如果把这个条件误写到 WHERE 里,空部门会被整体过滤掉,统计结果就不完整。
4. 核心功能拆解:从登录到员工列表的一次完整请求链路
4.1 db_helper:连接管理是整套系统最容易被抄走的部分
所有 dao 层代码都依赖一个公共的连接工具,它解决了两个问题:连接参数集中管理、连接用完必须关闭。看代码。
# db/db_helper.py import pymysql from db import db_config def get_connection(): """获取数据库连接""" conn = pymysql.connect( host=db_config.HOST, port=db_config.PORT, user=db_config.USER, password=db_config.PASSWORD, database=db_config.DATABASE, charset=db_config.CHARSET, cursorclass=pymysql.cursors.DictCursor, autocommit=False ) return conn def close_connection(conn): """关闭连接""" if conn: conn.close()参数说明:cursorclass 设为 DictCursor 后,查询结果返回的是字典列表,每一行的列名可以直接用 emp_name、dept_id 这样的键取到,代码可读性比元组高很多。autocommit 设为 False 是刻意为之,这样插入、更新、删除操作必须显式调用 conn.commit() 才生效,一旦操作中间出错可以在 except 里回滚。
在 tkinter 界面程序里,每次操作都新建连接、用完关闭,是课程设计级别的合理做法。不要考虑连接池,这种桌面程序的并发量根本用不到,保持代码简单更容易在答辩时讲清楚。
4.2 employee_dao:参数化查询是底线,字符串拼接会出大事
员工数据访问层封装了所有员工相关的 SQL 操作,这里挑最常用的两个方法看。
# dao/employee_dao.py from db.db_helper import get_connection, close_connection def find_all_employees(): """查询所有在职员工,关联部门名称""" conn = get_connection() try: with conn.cursor() as cursor: sql = """ SELECT e.emp_id, e.emp_name, e.gender, e.birth_date, e.phone, e.email, e.hire_date, e.salary, e.status, d.dept_name FROM employee e LEFT JOIN dept d ON e.dept_id = d.dept_id WHERE e.status = 1 ORDER BY e.emp_id DESC """ cursor.execute(sql) return cursor.fetchall() finally: close_connection(conn) def search_employees(dept_id=None, keyword=None, hire_date_start=None, hire_date_end=None): """根据部门、关键字、入职时间范围组合查询员工""" conn = get_connection() try: with conn.cursor() as cursor: sql = """ SELECT e.emp_id, e.emp_name, e.gender, e.birth_date, e.phone, e.email, e.hire_date, e.salary, e.status, d.dept_name FROM employee e LEFT JOIN dept d ON e.dept_id = d.dept_id WHERE e.status = 1 """ conditions = [] params = [] if dept_id: conditions.append("e.dept_id = %s") params.append(dept_id) if keyword: conditions.append("(e.emp_name LIKE %s OR e.phone LIKE %s)") params.append(f"%{keyword}%") params.append(f"%{keyword}%") if hire_date_start: conditions.append("e.hire_date >= %s") params.append(hire_date_start) if hire_date_end: conditions.append("e.hire_date <= %s") params.append(hire_date_end) if conditions: sql += " AND " + " AND ".join(conditions) sql += " ORDER BY e.hire_date DESC" cursor.execute(sql, tuple(params)) return cursor.fetchall() finally: close_connection(conn)逻辑说明:第一个方法做得干脆利落,直接查在职员工并按 emp_id 倒序,界面刷新时新添加的员工会显示在最上面。第二个方法是这个系统里最有含金量的代码——动态拼接 WHERE 条件。四个查询参数都可以为空,代码先定义一个空的 conditions 列表,然后逐个判断参数是否传值,有值就加一个条件占位符,同时把参数值放进 params 列表。最后用 AND 把这些条件拼起来。
参数化的核心是 execute(sql, tuple(params)) 这一步。%s 是占位符,执行的 SQL 和参数分开发给 MySQL 服务端,参数值永远不会被当作 SQL 代码解析。如果把参数直接格式化进 SQL 字符串,输入一个引号就能让语句语法错乱,输入“1' OR '1'='1”这样的内容就可能绕过滤条件查看全表数据,这是数据库项目绝对不允许出现的问题。
4.3 employee_service:将 dao 的结果变成界面能用的数据结构
dao 层返回的是字典列表,但界面表格需要的可能是元组列表,业务层负责转换。
# service/employee_service.py from dao import employee_dao def get_all_employees(): """获取所有在职员工,返回适合表格展示的元组列表""" rows = employee_dao.find_all_employees() result = [] for row in rows: result.append(( row["emp_id"], row["emp_name"], row["gender"], row["dept_name"], str(row["hire_date"]), row["salary"], )) return result def get_department_stats(): """统计每个部门的在职人数和平均薪资""" rows = employee_dao.get_department_stats() result = [] for row in rows: result.append(( row["dept_name"], row["emp_count"], float(row["avg_salary"]) if row["avg_salary"] else 0.0, )) return result在 DictCursor 模式下,row["salary"] 是 Decimal 类型,如果直接扔给 tkinter 的表格控件,有些控件显示会异常。这里有意识地把 Decimal 转成 float、date 转成 str,界面上展示的永远是最友好的数据类型。不要小看这层转换,课程设计里最常见的翻车点就是界面上显示出一串 Decimal('123.45') 这种奇怪文本,原因就是没做类型转换。
4.4 登录逻辑:SHA256 哈希比对,不存明文密码
登录流程在 user_service 里,它拿到界面输入的账号密码后,把密码做 SHA256 哈希,再到库里查询。
# service/user_service.py import hashlib from dao import user_dao def login(username, password): """登录校验,成功返回用户信息,失败返回 None""" user = user_dao.find_by_username(username) if not user: return None input_hash = hashlib.sha256(password.encode("utf-8")).hexdigest() if input_hash == user["password"]: return { "user_id": user["user_id"], "username": user["username"], "role": user["role"], "emp_id": user["emp_id"], } return None逻辑说明:先在库里按用户名查记录,查不到直接返回 None,不泄露用户是否存在的信息。查到了就把输入密码哈希化,和库里存的 64 位哈希字符串比对。两个哈希值一样才认为登录成功。role 字段存进返回结果里,主窗口根据角色决定哪些按钮可见,管理员能看到统计按钮,普通用户看不到。
4.5 从零跑通:初始化数据库、安装依赖、启动系统
项目刚下载下来时,运行之前需要三步准备。第一步是初始化数据库。打开 MySQL 命令行,执行 sql/init.sql 脚本。
mysql -u root -p < sql/init.sql第二步安装 Python 依赖。
pip install -r requirements.txt第三步修改 db_config.py 里的连接参数,把 HOST、PORT、USER、PASSWORD 改成自己本机 MySQL 的实际值,然后启动系统。
python app.py启动后先弹登录窗口,用初始化脚本里的 admin 账号登录,进入主界面后验证四个核心操作:添加一个员工,刷新列表能看到新记录;按部门筛选,列表会过滤;修改一个员工的薪资,重新查询确认已生效;删除一个员工,去数据库里执行 SELECT 确认物理删除成功。这些验证都通过,说明整个项目已经跑通。
5. 跑通项目避坑实录:依赖、编码与 SQL 兼容性问题排查
5.1 现象:pymysql 报错 Authentication plugin 'caching_sha2_password' cannot be loaded
原因:MySQL 8.0 默认的认证插件是 caching_sha2_password,而项目里安装的 PyMySQL 版本低于 1.0,不识别这种认证方式。
解决:升级 PyMySQL 到最新版本,要求 requirements.txt 里锁定 PyMySQL>=1.0.0。升级后重装依赖。
pip install --upgrade pymysql如果公司或学校机房不允许联网装包,备选方案是把对应的 MySQL 用户改成 mysql_native_password 认证,但更推荐直接升级驱动,因为后者不需要动数据库配置。
5.2 现象:控制台报错 Unknown database 'employee_db'
原因:没有执行初始化脚本,或者执行时连接的不是同一台 MySQL 实例。最常见的是只复制了 CREATE TABLE 语句,忘了执行最上方的 CREATE DATABASE。
解决:确认 init.sql 完整执行,然后在 MySQL 里执行 SHOW DATABASES 看看 employee_db 是否存在。如果存在但 Python 还是连不上,检查 db_config.py 里 DATABASE 字段是否写错,大小写都要一致。MySQL 的库名在 Linux 下区分大小写,Windows 下不区分,但保险起见保持一致。
5.3 现象:界面显示所有中文都变成问号
原因:建库时没指定 utf8mb4 字符集,或者 MySQL 服务端的 character_set_server 是 latin1。这是 Windows 中文环境下最高频的坑。
解决:最省事的方式是删掉数据库重新建——课设阶段数据量很小,删库重建成本极低。执行 DROP DATABASE employee_db,然后重新执行完整的 init.sql。注意检查 init.sql 里建库语句是否带了 DEFAULT CHARACTER SET utf8mb4,这是根上的修复。如果已有重要数据不能删,可以逐表转换字符集,但那是后话。
5.4 现象:日期筛选结果不对,比如选 2023-01-01 到 2023-01-31,1 月 31 日的数据查不出来
原因:界面传进来的是字符串形式的日期,比如“2023-01-31 00:00:00”,而阿里开发规范里推荐数据库层用 DATE 类型。当年份日期范围拼接在 SQL 里时,等于 2023-01-31 00:00:00 的值会小于 2023-01-31 23:59:59,导致最后一天的数据出不来。
解决:在 service 层做日期格式化,把传入的日期字符串统一规范成 YYYY-MM-DD,查询时把 end 日期加一天再比较,或者直接拼成 23:59:59。我一般会在 service 层写一个天数范围计算的工具方法,结束日期传入时自动加一天再查。
5.5 现象:离职员工点删除提示外键约束失败
原因:用户表里 emp_id 外键引用了 employee 表,有账号关联等场景下删除员工时,外键约束阻止了删除操作。
解决:业务上规定先解除关联再删除,也就是把 user 表里该员工对应的 emp_id 置为 NULL,然后再删员工。另一种方式是通过软件隔离——将 status 字段从 1 改成 0 做逻辑删除,不物理删除,这条更符合真实企业的数据管理习惯。课设答辩时能主动说出“我用逻辑删除避免外键阻塞和历史数据丢失”,是一个加分点。
6. 二次开发:把桌面 Demo 改造成可复用的服务化系统
6.1 把三层结构升级成四层:加一层接口层
现在系统的三层是 ui → service → dao,如果想把它改造成 Web 服务,不需要动 dao 层和 service 层,只需要在 ui 层的位置换成 Flask 路由。原来 ui 层做的是“用户点按钮然后刷新表格”,现在变成“接收 HTTP 请求然后返回 JSON”。
# api/employee_api.py from flask import Flask, request, jsonify from service import employee_service app = Flask(__name__) @app.route("/api/employees", methods=["GET"]) def list_employees(): keyword = request.args.get("keyword", "") dept_id = request.args.get("dept_id", type=int) rows = employee_service.search_employees( keyword=keyword, dept_id=dept_id ) return jsonify(rows) if __name__ == "__main__": app.run(port=5000, debug=True)这里可以看到最核心的一个思想:service 层返回值现在直接塞进 jsonify,连类型转换都省了,因为 Flask 内部会序列化。原来 tkinter 版本里需要手动把 Decimal 转 float 的逻辑,现在不需要了——jsonify 对 Decimal 类型自动做了处理。
6.2 权限控制从 role 字段升级成装饰器
原来的登录系统只有 admin 和 user 两个角色,主界面用 if 判断显示统计按钮。改造后可以在 Flask 里写一个权限装饰器,按角色控制接口访问权限。
from functools import wraps from flask import session def admin_required(func): @wraps(func) def wrapper(*args, **kwargs): if session.get("role") != "admin": return {"msg": "无权限访问"}, 403 return func(*args, **kwargs) return wrapper统计接口加上 @admin_required 之后,普通用户的 token 就算伪造也进不来。这套设计的价值在于:原来 view 层的接入,是永远绕不过数据库这一层的,把权限判断写在接口入口,可以让 service 层保持纯粹。
6.3 验证改造结果:用 curl 模拟一次完整调用
改造完成后,启动 Flask 服务,按顺序执行以下验证流程。
# 登录获取会话 curl -X POST http://127.0.0.1:5000/api/login -d "username=admin&password=admin123" # 查询员工(带关键字) curl "http://127.0.0.1:5000/api/employees?keyword=%E5%88%98" # 调用统计接口(需要管理员权限) curl http://127.0.0.1:5000/api/stats验证顺序不能乱:先验证登录能拿到会话,再验证普通查询能返回 JSON,最后验证权限拦截是否生效。我踩过这种改装的坑,有一次把 list_employees 写成了 GET 方法,结果前端老拿不到数据——F12 一看是 405 方法不允许,折腾了半天。从那以后我每次调试接口都强制走一遍“先 curl 裸调、再进前端页面”的流程,能省掉一半调试时间。希望这篇拆解能帮你在做课设或改造时少走一些弯路。
本文还有配套的精品资源,点击获取