Python+MySQL员工信息管理系统课设源码:链路拆解与排错

发布时间:2026/10/27 17:53:03
Python+MySQL员工信息管理系统课设源码:链路拆解与排错

简介:面向数据库课程设计与Python、MySQL入门实践的员工信息管理系统源码包,围绕员工、部门等实体完成库表设计与关系建模,并通过pymysql实现数据库连接、增删改查、事务处理和多表关联查询,适合需要完成课程设计或巩固数据库编程基础的学生参考。压缩包共9个文件,约6.71MB,包含2个py源码文件、2个doc文档、4个png图表及1个md说明;源码对应员工信息管理与账号管理模块,文档含课程设计报告,图表提供E-R图、数据流图与系统界面示意,便于对照理解表结构和业务流程。该资源已有4344人学习下载,读者可获得可运行的Python+MySQL项目骨架、数据库设计思路、SQL操作示例与课程设计文档框架,并借助图表和说明梳理实体关系、功能模块及异常处理要点,为后续扩展权限控制、索引优化等功能提供参考。

1. 员工信息管理系统:一个能直接跑通的课设基础盘

每年期末前后,总有一批课程设计的源码被丢过来让我帮忙跑一下,十个里面七八个是员工信息管理系统,打开一看,一半连数据库都连不上。这套基于 Python + MySQL 的版本属于少数能直接跑通的:没有花哨前端,纯控制台交互,但员工档案增删改查、部门归属、工号唯一性校验、软删除、分页查询这些课设里反复考的点都落地了,登录还做了密码哈希而不是明文存库。它适合两类人——一类是在交课设前想看清「完整增删改查到底该怎么分层」的,另一类是想拿一份能跑的基础盘,改出自己功能的人。下面我就按目录结构、请求链路、建库跑通、排错、二次开发这个顺序,把它拆开讲。

2. 技术栈与目录结构:Python + MySQL 这套组合是怎么搭起来的

这套代码的技术选型非常克制,克制到有点「反教学」——很多课设会堆 Flask、Vue、Redis,这里全都没有。它只用了三样东西:Python 标准库、PyMySQL 驱动、MySQL 5.7/8.0。这种选型的好处是,你不需要理解任何框架的生命周期,从main.py按 F5 进去,一路断点能打到 SQL 参数绑定的那一行,出了问题不会变成黑匣子。

2.1 依赖选型:PyMySQL + 原生 SQL 而不是 ORM

打开requirements.txt只有一行PyMySQL。很多人第一反应是「都 2025 年了还用原生 SQL 手写?」。我当时也这么想,直到把dao/目录看完才明白它的用意:课程设计的考察点本来就是 SQL 本身,GROUP BY部门人数统计、LEFT JOIN查部门下没有员工的情况、LIMIT offset, size分页,这些用 ORM 反而会被包装成看不见的东西,答辩时说不清。

配置文件的写法很典型:

# config.py —— 所有可变参数集中在这里,不进版本库的部分用环境变量覆盖 import os DB_CONFIG = { "host": os.getenv("EMP_DB_HOST", "127.0.0.1"), "port": int(os.getenv("EMP_DB_PORT", 3306)), "user": os.getenv("EMP_DB_USER", "emp_app"), "password": os.getenv("EMP_DB_PASS", "ChangeMe_2024"), "database": os.getenv("EMP_DB_NAME", "employee_ms"), "charset": "utf8mb4", # 不带这一项,中文姓名入库会变问号 }

参数说明:charset必须写utf8mb4而不是utf8,因为 MySQL 的utf8实际只有 3 字节,存不下 emoji 和部分生僻字,而utf8mb4是真正的 4 字节 UTF-8。port和user用os.getenv兜底,是为了换一台机器跑的时候只改环境变量、不改源码,这一点在机房和自用电脑之间来回切换时能省下不少血泪经验。

提示:password这种字段在真实项目里不应该有默认值,课设版为了方便才留着,二次开发第一步就该删除默认口令并强制走环境变量。

2.2 目录结构与模块职责

employee_ms/ ├── main.py # 入口,初始化日志、拉起主菜单 ├── config.py # 连接参数、分页大小、密码盐 ├── sql/ │ └── init.sql # 建库、建表、初始数据 ├── db.py # 连接工厂 + 事务上下文 ├── dao/ │ ├── employee_dao.py # 员工表 CRUD │ └── dept_dao.py # 部门表 CRUD ├── service/ │ └── employee_service.py # 校验、组合、软删除逻辑 ├── entity/ │ └── employee.py # 数据载体,只有 __slots__ 和字段 └── ui/ ├── menu.py # 主菜单与路由 └── employee_view.py # 列表渲染、分页、输入回显

模块职责是严格单向的:ui → service → dao → db,反向依赖一律不允许。我第一次看的时候专门 grep 了一遍import关系,确认dao层没有反向 importui,才敢说这个分层是干净的。每一层的失败信息在上一层的处理方式也不同——dao抛pymysql.MySQLError,service转成自己的BizError,ui只负责打印中文提示,这样业务层不会被数据库异常污染。

2.3 三张核心表的设计

sql/init.sql里最主要的是department和employee两张表,加上一张sys_user做登录。表设计里有两个点值得学:软删除字段is_deleted和防重复的UNIQUE约束。

-- sql/init.sql 精简版 CREATE DATABASE IF NOT EXISTS employee_ms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE employee_ms; CREATE TABLE department ( dept_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, dept_name VARCHAR(50) NOT NULL UNIQUE, manager VARCHAR(30) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE employee ( emp_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, emp_no VARCHAR(16) NOT NULL UNIQUE, -- 工号唯一,业务层再校验一次 real_name VARCHAR(30) NOT NULL, gender TINYINT NOT NULL DEFAULT 0, -- 0 未知 / 1 男 / 2 女 phone VARCHAR(20) DEFAULT NULL, dept_id INT UNSIGNED NOT NULL, position VARCHAR(40) DEFAULT NULL, hire_date DATE NOT NULL, salary DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- 金额别用 FLOAT is_deleted TINYINT NOT NULL DEFAULT 0, -- 软删除标记 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_dept (dept_id), CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数选择的原因:salary用DECIMAL(10,2)不用FLOAT,是因为浮点在金额累计时会出现0.1+0.2≠0.3的经典问题,答辩时如果被追问这一条,是加分项;emp_no加UNIQUE而不是只靠业务层判重,是防止并发插入时两次查重都通过、最后插入两条相同工号;is_deleted存小整数而不是BOOLEAN,是为了跟 MySQL 5.7 兼容,某些版本对BOOLEAN别名只有TINYINT(1)的等价处理,语义上容易混淆。KEY idx_dept是给「按部门查员工」这个高频查询加的索引,不加的话数据量一过千,列表页会明显卡顿。

3. 核心模块拆解:一次「新增员工」请求走完的完整链路

只看目录是看不出门道的,真正能抄作业的是把一个动作拆开。我选了「新增员工」这条最长的链路:从菜单输入工号到数据落库,中间要过四层,每一层都藏着一两个容易翻车的点。

3.1 连接层封装:db.py 里的游标管理与事务边界

db.py是整个项目的地基,它用上下文管理器管住连接和游标,避免「忘了 close 导致连接池耗尽」这种低级错误。

# db.py from contextlib import contextmanager import pymysql from config import DB_CONFIG @contextmanager def get_cursor(commit: bool = False): """每次调用拿一条新连接;commit=True 时成功自动提交、失败自动回滚""" conn = pymysql.connect( **DB_CONFIG, cursorclass=pymysql.cursors.DictCursor, # 返回 dict,方便按字段名取值 autocommit=False # 关键:默认手动提交 ) cur = conn.cursor() try: yield cur if commit: conn.commit() except Exception: conn.rollback() # 任何异常都回滚,不吃后悔药 raise finally: cur.close() conn.close()

逻辑说明:DictCursor让SELECT返回的每一行是{"emp_no": "E001", "real_name": "张三"}这种字典,业务代码里不用记列顺序,改字段时不容易错位。autocommit=False加上显式commit参数,是把事务边界交给调用方决定——查询用commit=False,插入/更新用commit=True。except里先rollback再raise,保证异常不被吞掉,上层还能打印原因。

参数怎么改:如果把项目挪到连接频繁的场景,这种「一次调用一条连接」的写法会拖慢速度,常见做法是换成DBUtils.PooledDB维护一个小连接池,mincached=2, maxcached=5就够课设用了。我一般会在config.py里加一个POOL_SIZE,方便一键切换。

3.2 DAO 层:参数化 SQL 与软删除过滤

employee_dao.py里所有 SQL 都用%s占位,不用 f-string 拼字符串。这一点在答辩里几乎必问。

# dao/employee_dao.py from db import get_cursor INSERT_SQL = """ INSERT INTO employee (emp_no, real_name, gender, phone, dept_id, position, hire_date, salary) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) """ def insert(emp: dict) -> int: """插入一名员工,返回自增主键 emp_id""" with get_cursor(commit=True) as cur: cur.execute(INSERT_SQL, ( emp["emp_no"], emp["real_name"], emp["gender"], emp["phone"], emp["dept_id"], emp["position"], emp["hire_date"], emp["salary"], )) return cur.lastrowid def page(keyword: str, offset: int, size: int): """分页查询,关键字匹配工号或姓名;默认过滤软删除记录""" sql = """ SELECT e.emp_id, e.emp_no, e.real_name, e.gender, d.dept_name, e.position, e.hire_date FROM employee e JOIN department d ON d.dept_id = e.dept_id WHERE e.is_deleted = 0 AND (%s = '' OR e.emp_no LIKE %s OR e.real_name LIKE %s) ORDER BY e.emp_id DESC LIMIT %s OFFSET %s """ like = f"%{keyword}%" with get_cursor() as cur: cur.execute(sql, (keyword, like, like, size, offset)) return cur.fetchall()

逻辑说明:INSERT_SQL用%s占位而不是字符串拼接,是为了防止real_name里出现单引号把 SQL 截断(SQL 注入的第一课)。page里的%s = '' OR ...是个技巧:关键字为空时整条条件恒真,不用在 Python 里拼两套 SQL,减少分支。is_deleted = 0写在WHERE最前面,保证软删除数据不会出现在列表里——这是初学者最容易漏的一条,一漏就会「删了还在」。

参数调整:LIMIT %s OFFSET %s在 MySQL 里占位符是允许的,但顺序必须是LIMIT size OFFSET offset,写反了不报错但结果错。offset = (page_no - 1) * size,size建议从config.py读,别写死 10。

3.3 Service 层:校验规则与重复工号检测

service层是「翻车最多」的一层,因为很多人会跳过它直接让ui调dao。这个项目的employee_service.py至少做了三件事:字段非空、工号唯一、部门存在。

# service/employee_service.py from dao import employee_dao, dept_dao from db import get_cursor class BizError(Exception): """业务异常,ui 层捕获后直接打印""" def create_employee(form: dict) -> int: # 1. 必填校验 for field in ("emp_no", "real_name", "dept_id", "hire_date"): if not str(form.get(field, "")).strip(): raise BizError(f"字段 {field} 不能为空") # 2. 工号唯一(数据库 UNIQUE 是最后防线,这里先给出友好提示) with get_cursor() as cur: cur.execute( "SELECT 1 FROM employee WHERE emp_no = %s AND is_deleted = 0", (form["emp_no"],) ) if cur.fetchone(): raise BizError(f"工号 {form['emp_no']} 已存在") # 3. 部门必须存在 if not dept_dao.exists(form["dept_id"]): raise BizError(f"部门 {form['dept_id']} 不存在") return employee_dao.insert(form)

逻辑说明:第 2 步的查重把is_deleted = 0也带上了,意味着「已删除员工的工号可以复用」——这是个需要跟需求方确认的点,我见过两种做法,一种允许复用(历史记录归档),一种不允许(保留审计痕迹)。这个项目选的是前者,代码里注释也写清楚了。第 3 步查部门存在性,是为了在插入前给中文提示,否则数据库外键会抛一个英文的MySQLError 1452,用户体验直接崩。

参数怎么改:如果要加「入职日期不能晚于今天」,在必填校验后面补一句if form["hire_date"] > date.today(): raise BizError(...),两行的事。BizError这种自定义异常的好处是ui层可以except BizError as e统一转中文,不用分析哪个是数据库异常哪个是业务异常。

3.4 菜单层:输入回显与异常兜底

ui/employee_view.py负责把用户输入变成一个dict,再交给service。它最容易翻车的地方是「用户输了个空格」或者「输入q想返回却被当成姓名」。

# ui/employee_view.py from service.employee_service import create_employee, BizError def input_add_employee(): print("=== 新增员工(输入 q 取消)===") form = {} for field, label in [ ("emp_no", "工号"), ("real_name", "姓名"), ("gender", "性别(0/1/2)"), ("phone", "电话"), ("dept_id", "部门ID"), ("position", "职位"), ("hire_date", "入职日期(YYYY-MM-DD)"), ("salary", "月薪"), ]: raw = input(f"{label}: ").strip() if raw.lower() == "q": print("已取消") return form[field] = raw try: emp_id = create_employee(form) print(f"新增成功,emp_id = {emp_id}") except BizError as e: print(f"操作失败:{e}") except Exception as e: print(f"系统异常:{e}") # 兜底,避免一次报错把整个程序打断

逻辑说明:raw.lower() == "q"这个出口很关键,没有它的话用户输到一半想退出只能Ctrl+C,在 Windows 上会直接抛KeyboardInterrupt把主循环打断。except BizError和except Exception分开写,是让业务错误给出中文原因、系统错误给出原始信息,前者给用户看、后者给自己调试看。form[field] = raw全存字符串,类型转换统一交给service,避免ui层做业务判断。

参数怎么改:gender和dept_id这里没做输入类型校验,稳妥做法是加一层int()转换并try/except ValueError,或者干脆用PyInquirer这种交互库做选项。不过课设场景下,多一行键盘输入就多一个 bug 来源,我一般保持这种最朴素的写法,等交完再优化。

4. 跑起来:从建库建表到第一次登录成功

上面的代码看得再明白,不跑一遍都是纸上谈兵。这一章按「新机器、干净环境」从头走一遍,每一步都说清楚失败时该看什么。

4.1 环境准备与依赖安装

Python 版本建议 3.8~3.11,pymysql对 3.12 也支持,但部分机房的 3.7 以下版本装最新pymysql会报错。命令按顺序执行:

# 1. 确认 Python 版本 python --version # 期望 3.8 及以上 # 2. 建虚拟环境(可选,但强烈建议) python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate # 3. 安装依赖 pip install PyMySQL

如果pip install卡在下载阶段,是网络问题,与代码无关,换国内镜像或多试几次即可。装完执行python -c "import pymysql; print(pymysql.__version__)"能打印版本号,就说明驱动就位。

4.2 建库建表与初始数据

MySQL 服务起来后,用命令行或 Navicat 连上去,执行sql/init.sql:

# 命令行执行整个脚本 mysql -u root -p < sql/init.sql # 进入库确认表建好了 mysql -u root -p -e "USE employee_ms; SHOW TABLES;"

期望看到department、employee、sys_user三张表。接着手工插两条部门数据,否则新增员工时dept_id一定找不到:

INSERT INTO department (dept_name, manager) VALUES ('研发部', '某负责人'), ('行政部', '某负责人');

init.sql里如果没有sys_user的初始记录,还要补一条登录账号,密码字段必须是哈希后的字符串,不能直接塞明文,否则登录逻辑校验不过。

4.3 配置连接参数并首次运行

config.py里的user、password改成你自己 MySQL 的账号。如果不想改源码,用环境变量:

# Windows PowerShell $env:EMP_DB_USER="root"; $env:EMP_DB_PASS="你的密码" # macOS / Linux export EMP_DB_USER=root EMP_DB_PASS='你的密码' python main.py

启动后应该看到主菜单,输入管理员账号登录。首次登录报错的话,按这个顺序排查:先看config.py里的host是不是127.0.0.1(不是localhost,某些环境会走 socket 而不是 TCP),再看 MySQL 服务是否在运行,最后看账号是否有employee_ms库的访问权限。

注意:MySQL 8.0 默认认证插件是caching_sha2_password,老版本pymysql可能不兼容,会报Authentication plugin ... cannot be loaded。升级pymysql到最新版,或者给账号执行ALTER USER ... IDENTIFIED WITH mysql_native_password BY '...',两种做法都不影响代码。

4.4 冒烟测试:五条命令验证系统可用

跑通不代表功能正常。我一般会按下面五步做冒烟,每一步都对应一个模块:

步骤操作验证的模块
1登录管理员账号sys_user表 + 密码哈希
2新增一名员工service校验 +dao插入
3同一工号再新增一次唯一约束 + 中文提示
4列表页查关键词「张」LIKE查询 + 分页
5删除该员工后刷新列表软删除过滤

五步全过,说明主链路是通的。任何一步失败,回到对应模块的代码看,比盲改强。

5. 避坑与排查:这套源码跑不通时先看这五处

我在不同机器上跑这套代码,失败原因来来回回就那么几种。按「现象 → 原因 → 解决」整理成五条,遇到问题时对着看。

5.1 现象:中文姓名入库变问号,列表页显示???

原因:建表时CHARSET用了utf8(3 字节),而 Python 端连接参数里的charset又写成了utf8mb4,两边字符集不一致,MySQL 在转换时直接丢弃无法映射的字节。另一种情况是init.sql开头没写SET NAMES utf8mb4,脚本执行时的客户端默认字符集污染了数据。

解决:CREATE DATABASE和CREATE TABLE都显式写DEFAULT CHARSET=utf8mb4,config.py里charset同步改成utf8mb4,脚本开头加SET NAMES utf8mb4;。已污染的旧数据没法自动恢复,只能删库重建。

5.2 现象:启动报Can't connect to MySQL server on '127.0.0.1' (10061)

原因:这个错误码 10061 是 Windows 上的「连接被拒绝」,八成是 MySQL 服务没启动;少数情况是端口被改成了 3307 但配置里还是 3306。

解决:services.msc里确认 MySQL 服务是运行状态;命令行netstat -ano | findstr 3306看端口是否在监听;如果端口不同,改config.py的port而不是去改 MySQL。

5.3 现象:删除部门时抛Cannot delete or update a parent row: a foreign key constraint fails

原因:employee表上有fk_emp_dept外键指向department,被引用的部门不能直接删。这是 InnoDB 外键的默认行为,不是 bug。

解决:业务上应该先判断该部门下是否还有is_deleted = 0的员工,有就提示「请先转移员工」;如果确定要级联删,把外键改成ON DELETE CASCADE,但课设场景我一般不建议——级联删会把员工数据一起冲掉,答辩时被问「误删怎么恢复」就答不上来了。

5.4 现象:插入员工报Incorrect date value: '' for column 'hire_date'

原因:MySQL 的sql_mode里带了STRICT_TRANS_TABLES,空字符串不能隐式转成DATE;而ui层没拦住空日期,直接把''传到了dao。

解决:service层在必填校验里加hire_date(这份代码已经加了),或者在INSERT前用datetime.strptime校验格式,不合格直接抛BizError给中文提示,别等数据库报英文错误。

5.5 现象:登录一直提示密码错误,但数据库里账号确实存在

原因:两种可能,一种是sys_user.password存的是明文,而登录逻辑用hashlib.sha256(salt + pwd)去比对,永远不相等;另一种是密码哈希时用了随机盐,但盐没存进表,校验时又用了新生成的盐。

解决:检查init.sql里初始账号的密码字段是不是哈希值,如果是明文,用项目的hash_password()函数跑一遍再更新;如果用随机盐,sys_user表必须有一个salt列存下来,校验时取出来用,不能每次现生成。

6. 二次开发的改造思路:从「能跑」到「像回事」

课程设计交上去只是起点,这套代码的结构留了不小的改造空间。按投入产出比排,我建议从下面三个方向动,改动量从小到大、对答辩加分的幅度从大到小。

第一个方向是把控制台换成 Web 界面,用 Flask 套一个最小前端。service和dao一行都不用改,只需要新增app.py把create_employee()挂到/employee/add这个路由上,用request.form接参数,返回redirect('/employee/list')。改造的关键是异常处理——控制台用print,Web 必须用flash()或 JSON 返回,BizError要单独捕获并映射成 400 状态码。验证方法很简单:打开浏览器能增能查能删,就说明链路通了。

第二个方向是加一个统计接口,这是答辩时最能拉开差距的一条。在dao层加一个count_by_dept():

# dao/employee_dao.py 追加 def count_by_dept(): sql = """ SELECT d.dept_name, COUNT(e.emp_id) AS total FROM department d LEFT JOIN employee e ON e.dept_id = d.dept_id AND e.is_deleted = 0 GROUP BY d.dept_id, d.dept_name ORDER BY total DESC """ with get_cursor() as cur: cur.execute(sql) return cur.fetchall()

注意LEFT JOIN的ON条件里带is_deleted = 0,不能写进WHERE,否则没有员工的部门会被整行过滤掉,统计结果少几个部门。这是 SQL 里最容易翻车的经典差异,我在这上面踩过不止一次。改完之后用一条INSERT造成「研发部 5 人、行政部 0 人」的数据,看返回结果里行政部是不是也在,就能验证。

第三个方向是补上审计字段,给employee加updated_at和updated_by,所有更新操作走同一个update方法并刷新updated_at。这一步看着简单,但它逼你把所有散落的UPDATE语句收敛到一处,是理解「为什么要分层」最直接的体验。

注意:三个方向不要同时改。我一般先用 Git 打一个v0.1-console的 tag,改 Web 就开一个feat/web分支,改崩了随时能回退。

从那以后我每次接一个带数据库的项目,上线前都强制走一遍「建库 → 插入初始数据 → 五步冒烟」,少跑一步都会在验收现场被现场抓包。这套课设代码的价值不在于它多完整,而在于每一层的坑都摆在明面上,你改一次就记住一次。希望帮到你。

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