最近被问得最多的一个选题是:“我想做一个员工管理系统,后端能不能用Python+Django?”
我一向直接回答:能,而且对于绝大多数人来说,Django是这类项目的最佳选择。很多同学手里的那套“基于Python+Django的员工管理系统”,从毕业设计到公司内部Demo,几乎都长一个样:源码、论文(lw)、部署文档、讲解视频。但真正能把这套东西讲清楚、能本地跑起来、能部署上服务器、能在答辩时把话说圆的人,并不多。
这篇文章我就围绕这套系统,把“为什么选Django”“数据模型怎么设计”“核心功能怎么写”“项目怎么跑通”“服务器怎么部署”这几件事一次性讲透。打算照着做毕设、练手Django,或者想快速搭建一套带登录、员工管理、考勤统计界面的同学,可以直接把下面的思路当参考。
先说明一点,我不会去讨论网上流传源码里某段代码写得对不对,因为别人给的代码你直接复制粘贴是学不到东西的。更靠谱的做法是,先理解这套系统的骨架,然后自己在关键节点上动手改一版。下面所有内容,都是围绕这套员工管理系统展开。
1. 员工管理系统这种“老需求”,为什么我总是推荐Python+Django
1.1 Django自带的东西,几乎就是为这类系统准备的
员工管理系统看起来简单,但你把需求拆开会发现,它一点都不“简单”。管理员登录、员工信息增删改查、部门管理、考勤记录、工资信息、公告发布,哪个模块能少?权限隔离、表单校验、数据库关联、页面模板,这些东西如果全从零开始手写,工作量是非常可观的。
Django的优势在于,它把这些最耗时间的“基础设施”全部内置了。ORM负责操作数据库,你不用手写一堆SQL;admin后台直接把模型管理界面生成出来;auth模块自带用户登录、退出、权限判断;模板系统可以做一个像样的管理后台页面;CSRF、SQL注入、XSS的防护也已经内建好了。换句话说,你要做的不是“造轮子”,而是“组装轮子”。
相比Flask,Django虽然重一点,但正因为重,才适合这种模块多、需要后台管理、需要权限控制的项目。Flask的灵活性强,可真要做员工管理,你还得自己折腾Flask-SQLAlchemy、Flask-Login、WTForms,折腾完才发现时间已经过去一半。
1.2 版本搭配上的实际经验:别一上来就装最新版
我看到太多人栽在版本上。项目下载下来,里面写着python3.8 + Django 2.2,你非要装Python 3.12 + Django 5.0跑,然后发现一堆第三方依赖不兼容,到处报错,最后直接放弃。
这里先给出一张兼容性选型表,是我这些年自己测试过、也经常用在推荐里的组合:
| 项目类型 | 推荐组合 | 说明 |
|---|---|---|
| 本地开发学习 | Python 3.8 ~ 3.10 + Django 2.2/3.2/4.2 | 老项目源码最常用的范围 |
| 新写个人项目 | Python 3.10 + Django 4.2 LTS | 稳定,文档多,第三方兼容好 |
| 追求新特性 | Python 3.11/3.12 + Django 5.x | 部分老库可能没跟上,谨慎 |
| 数据库 | SQLite搭载本地,MySQL部署 | 两者Django都原生支持 |
Django 4.2是长期支持版本,修复了历史遗留问题,又没有太多激进改动,后端开发和毕业设计用这个版本最稳。Django 3.2同样推荐,很多老源码基于它。但如果你看到源码头上有django.core.exceptions.ImproperlyConfigured之类的报错,大多数原因就是Django版本升级后某些API变了,尤其要注意url()与path()的混用。
1.3 “源码+论文+部署文档+讲解”这套交付,价值在哪
很多学生下载下来的员工管理系统资料包,体积不小,但真正有含金量的排序应该是:部署文档>源码>讲解视频>论文。为什么这么说?因为对初学者来说,最难的往往是环境搭建和跑通流程。如果部署文档写得清楚,源码质量也不差,你复现的过程就会顺利很多,后面改代码才有成就感。
论文反而是最后才需要的,它是把你做过的东西用文字讲清楚,而不是写一堆网上抄来的背景。所以我后面专门有一部分会聊怎么把资料包里的论文变成自己的东西。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先把需求理顺:功能模块和数据模型怎么设计才不会返工
2.1 员工管理系统最核心的链路是什么
你拿到一套源码后,第一件事不是立刻点运行,而是顺一遍业务链路。员工管理系统无论做到多复杂,最核心的链路只有一条:
管理员登录 → 创建部门 → 添加员工 → 把员工分配到部门 → 日常记录考勤/薪资 → 随时查询和导出。
所有花哨的功能,都是这条链路上长出来的叶子。明白了这条链路,你去看项目的models.py,基本就能猜到里面会有哪几张表。
通常最少会有这几张表:
- 用户表/管理员表:负责登录和权限,Django自带的
auth.User扩展即可。 - 部门表:字段最少要有部门名称、负责人、创建时间。
- 员工表:姓名、工号、性别、手机号、邮箱、所属部门、职位、入职日期、学历、状态。
- 考勤表:员工外键、日期、上班打卡时间、下班打卡时间、状态。
- 薪资/工资表:员工外键、月份、基本工资、绩效、实发工资。
2.2 Django模型设计的几个关键细节
很多初学者设计表喜欢乱放字段,比如把部门名字直接写进员工表,结果部门改名要批量更新员工记录。正确的做法是建立外键关系。
以员工和部门的关系为例:
python复制from django.db import models
class Department(models.Model):
name = models.CharField('部门名称', max_length=50, unique=True)
manager = models.CharField('负责人', max_length=20, blank=True)
create_time = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
db_table = 'department'
verbose_name = '部门'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Employee(models.Model):
GENDER_CHOICES = [
('M', '男'),
('F', '女'),
]
emp_no = models.CharField('工号', max_length=20, unique=True)
name = models.CharField('姓名', max_length=20)
gender = models.CharField('性别', max_length=1, choices=GENDER_CHOICES, default='M')
phone = models.CharField('手机号', max_length=11)
department = models.ForeignKey(
Department,
on_delete=models.PROTECT,
verbose_name='所属部门',
related_name='employees'
)
position = models.CharField('职位', max_length=50)
hire_date = models.DateField('入职日期')
status = models.BooleanField('在职状态', default=True)
class Meta:
db_table = 'employee'
verbose_name = '员工'
verbose_name_plural = verbose_name
def __str__(self):
return f'{self.emp_no} {self.name}'
外键上要注意两点。第一,on_delete不要无脑用CASCADE。部门一旦级联删除,你把整个部门删掉,下面所有员工也跟着没了,有时候这是一件非常危险的事。现实中更合理的是用PROTECT或SET_NULL,保证有员工挂在部门下时不允许清理,这才是数据该有的严谨性。
第二,工号要加unique=True,否则员工表里可能出现两个完全相同的工号,后面做考勤、薪资关联时就会一对多产生歧义。
如果要加考勤记录,还建议用数据库层的联合唯一约束来防止一天重复打卡:
python复制class Attendance(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE, verbose_name='员工')
work_date = models.DateField('日期')
check_in = models.DateTimeField('上班打卡', null=True, blank=True)
check_out = models.DateTimeField('下班打卡', null=True, blank=True)
class Meta:
db_table = 'attendance'
verbose_name = '考勤'
verbose_name_plural = '考勤'
constraints = [
models.UniqueConstraint(fields=['employee', 'work_date'], name='unique_attendance_per_day')
]
这个UniqueConstraint就是你防止重复记录的好帮手,比在业务代码里手工判断数据是否存在要靠谱得多。
2.3 数据库选SQLite还是MySQL,不能拍脑袋
如果只是本科毕业设计、课程设计或者本地练手,SQLite完全够用。Django默认也用它,数据库文件就是一个.db文件,迁移、测试都特别轻量,不用装服务。
但项目一旦要部署到云服务器,我通常建议换成MySQL,因为SQLite在并发写、多进程访问上确实弱一些。Nginx + Gunicorn跑起来以后,多个进程同时写SQLite容易出现“database is locked”。
换MySQL的时候注意,驱动不要用老掉牙的mysql-python,建议用mysqlclient,或者PyMySQL。在Django的__init__.py里加上:
python复制import pymysql
pymysql.install_as_MySQLdb()
然后settings.py里改:
python复制DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'employee_db',
'USER': 'root',
'PASSWORD': '你的密码',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}
需要注意MySQL建库时指定utf8mb4字符集,不然中文数据会变成随机问号。
3. 登录、列表、导出这些核心功能,代码层面怎么实现不跑偏
3.1 登录鉴权:用Django自带的auth就行,别再手写session
有些初学者一上来就自己想Session机制,甚至把用户名密码塞进去自己校验,工作量大还容易出漏洞。员工管理系统的登录直接用django.contrib.auth就好。
视图里核心就几行:
python复制from django.contrib.auth import login, logout
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
def login_view(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
user = authenticate(request, username=username, password=password)
if user:
login(request, user)
return redirect('employee_list')
error = '用户名或密码错误'
else:
error = ''
return render(request, 'login.html', {'error': error})
@login_required
def logout_view(request):
logout(request)
return redirect('login')
对应需要进入权限控制的视图,直接加装饰器:
python复制from django.contrib.auth.decorators import login_required
@login_required
def employee_list(request):
...
同时记得在settings.py里加LOGIN_URL = '/login/',否则未登录用户会被Django踢到默认的/accounts/login/页面,新手经常在这卡住。
实际项目中只有一个“能登录”还不够,建议在员工列表页的模板里用request.user.is_staff或用户组做好区分。比如普通员工登录后只能看自己的考勤和工资,管理员才能管理全部员工。这一步虽然不难,但答辩时被问“你的权限是怎么设计的”就有了真正的内容。
3.2 员工列表的搜索分页,坑一般都出在细节上
列表功能看似是普通查询,但要把它做得不丢条件、不报注入错误,也需要些思路。
搜索场景通常是:员工姓名、工号、部门三个条件任意组合。用Q对象来做模糊查询是最省事的方案:
python复制from django.db.models import Q
from django.core.paginator import Paginator
from .models import Employee
def employee_list(request):
keyword = request.GET.get('keyword', '').strip()
department_id = request.GET.get('department', '').strip()
employees = Employee.objects.select_related('department').all()
if keyword:
employees = employees.filter(
Q(name__icontains=keyword) | Q(emp_no__icontains=keyword)
)
if department_id:
employees = employees.filter(department_id=department_id)
paginator = Paginator(employees, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'employee/list.html', {
'page_obj': page_obj,
'keyword': keyword,
'department_id': department_id,
})
这里有个很典型的坑:翻页时搜索条件丢了。为什么?因为你在模板里生成的翻页链接只带了一个page参数:
html复制<a href="?page={{ page_obj.next_page_number }}">下一页</a>
正确的做法是把原查询参数和页码一起拼到URL上:
html复制<a href="?keyword={{ keyword }}&department={{ department_id }}&page={{ page_obj.next_page_number }}">下一页</a>
在模板里遍历员工时,因为已经在视图层用了select_related('department'),模板里直接employee.department.name不会产生重复的额外查询,页面性能会好很多。
3.3 表单校验和Excel导出,是答辩中最容易加分的点
很多源码包自带的员工新增/编辑功能就是手写request.POST.get然后Employee.objects.create,没有校验,也没有错误提示。数据录入一点小问题就报错,体验很差。一个合理的做法是用ModelForm统一处理表单和校验逻辑。
我推荐在做新增和编辑时把公共表单抽象出来,比如:
python复制from django import forms
from .models import Employee
class EmployeeForm(forms.ModelForm):
class Meta:
model = Employee
fields = ['emp_no', 'name', 'gender', 'phone', 'department', 'position', 'hire_date', 'status']
widgets = {
'hire_date': forms.DateInput(attrs={'type': 'date'}),
}
def clean_phone(self):
phone = self.cleaned_data.get('phone')
if phone and not phone.isdigit():
raise forms.ValidationError('手机号必须为数字')
return phone
这样既不用手写前端日期控件,又能在clean_phone里做手机号格式校验。视图里提交后先判断form.is_valid(),再决定form.save()或把错误信息渲染回页面,操作清晰且有安全保障。
导出Excel能做的事情很多。最简单的官方方案是导出CSV:
python复制import csv
from django.http import HttpResponse
def export_employees_csv(request):
response = HttpResponse(content_type='text/csv; charset=utf-8')
response['Content-Disposition'] = 'attachment; filename="employees.csv"'
writer = csv.writer(response)
writer.writerow(['工号', '姓名', '性别', '部门', '职位'])
employees = Employee.objects.select_related('department').all()
for emp in employees:
writer.writerow([emp.emp_no, emp.name, emp.get_gender_display(), emp.department.name, emp.position])
return response
有的项目也喜欢用openpyxl导出成真正的.xlsx文件,我建议作为拓展功能做一下。你用CSV在答辩时打开可能被识别成“记事本文件”,视觉上不如Excel表格正式。
4. 从源码到本机跑通:完整复现流程与常见错误排查
4.1 Python虚拟环境这一步,决定后面少踩多少坑
复现项目前,先把Python环境单独隔离。直接在全局环境里pip install django的人,往往会在不同项目间把依赖版本搞混,最后调试到崩溃。
最简单的操作:
bash复制# Windows
python -m venv venv
venv\Scripts\activate
# macOS / Linux
python3 -m venv venv
source venv/bin/activate
激活后命令行前面会多一个(venv),这时候再安装依赖:
bash复制pip install -r requirements.txt
如果requirements.txt缺失,那你需要手动按项目源码导入的模块补装。对Django员工管理系统来说,基础依赖至少包括:
text复制Django>=3.2,<5.0
mysqlclient>=2.1.0
# 可能还有djangorestframework、openpyxl、Pillow等,以源码实际目录为准
国内网络环境下安装慢的话可以加清华镜像源:
bash复制pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
这里有个亲测的经验:拿到源码后发现python manage.py runserver明明执行了,却提示ModuleNotFoundError: No module named 'django',八成是因为你没激活虚拟环境,或者系统里同时存在多个Python版本。可以在虚拟环境里先执行python -m pip install --upgrade pip,再装一次依赖,基本就能解决。
4.2 迁移、创建管理员、跑起来,顺序别搞反
项目根目录下通常有个manage.py,所有Django命令都基于它。把这一步操作顺序记得滚瓜烂熟:
bash复制# 1. 检查依赖和版本
pip list | grep -i django
# 2. 生成数据库迁移文件,并把表结构同步到数据库
python manage.py makemigrations
python manage.py migrate
# 3. 创建管理员账号
python manage.py createsuperuser
# 4. 启动本地开发服务器
python manage.py runserver
然后浏览器打开http://127.0.0.1:8000看到系统首页,再从http://127.0.0.1:8000/admin进入Django后台。很多源码在首次运行时没有初始数据,但会通过fixtures或import_data.py脚本提供测试数据,所以请在项目目录里找一下有没有fixtures目录或者init_data.py之类的文件。
这类脚本执行方式通常是:
bash复制python manage.py loaddata initial_data.json
或者:
bash复制python init_data.py
建议多花五分钟看看源码里的urls.py,把路由结构和视图函数对应关系理清楚。这样一旦页面报错,你能第一时间知道问题在哪个模块。
4.3 本机运行常见的错误,基本上就这几类
| 报错现象 | 原因与处理方式 |
|---|---|
ModuleNotFoundError: No module named 'django' |
未激活虚拟环境或依赖没装成功,检查当前解释器路径 |
django.db.migrations.exceptions.InconsistentMigrationHistory |
已有数据库记录与迁移文件不一致,可以先备份旧库再删除重来,或手动执行makemigrations |
Forbidden (403): CSRF verification failed |
提交表单缺少{% csrf_token %},在模板的form里加上 |
DisallowedHost at / |
ALLOWED_HOSTS没包含当前域名或IP,本地改成ALLOWED_HOSTS = [],部署后需要填域名 |
TemplateDoesNotExist |
模板目录配置错误,检查settings.py的TEMPLATES中的DIRS |
| 静态文件图片不显示 | STATICFILES_DIRS或STATIC_ROOT没设置对,页面加载404 |
| 中文乱码 | 数据库连接字符集不是utf8mb4,MySQL建库时指定字符集最稳妥 |
我见过最哭笑不得的情况:有同学把系统启动后页面样式全丢了,跑过来问“代码是不是坏的”。其实不是,他只是没执行collectstatic,Django开发模式下静态文件路径需要定义正确。本地调试时可以不用太纠结,开发服务器默认能带static文件,但如果所有图片CSS都坏掉了,十有八九是settings.py里STATIC_URL以及模板{% load static %}的问题。
5. 真正部署到服务器,最容易卡住的是这几步
5.1 关掉DEBUG后系统为什么“裂开”了
很多人在本地运行一切正常,高高兴兴部署到云服务器,一访问直接报DisallowedHost,或者页面能打开但是所有静态资源都404。这是因为Django在DEBUG=False下不再帮我处理静态文件,也不会允许任意域名访问。
此时请在settings.py中做三件事:
python复制DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', '你的服务器公网IP']
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = '/media/'
然后执行:
bash复制python manage.py collectstatic
静态文件会统一收集到staticfiles目录。这一步做完,Django自身还是不建议直接对外服务静态文件的,所以真正的收官动作要交给Nginx。
5.2 Nginx + Gunicorn的组合,我建议手动配置一次
网上很多部署文档推荐宝塔面板,面板确实方便,但有时为了兼容Python 3.10还得自己手动编译,过程很痛苦。如果你只是想用一台云服务器把项目跑起来,手动配置Nginx + Gunicorn,几次之后就理解了原理。
先生成Gunicorn配置并启动:
bash复制pip install gunicorn
gunicorn 项目名.wsgi:application --bind 127.0.0.1:8000
然后Nginx方向代理配置:
nginx复制server {
listen 80;
server_name yourdomain.com;
location /static/ {
alias /你的项目路径/staticfiles/;
}
location /media/ {
alias /你的项目路径/media/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
配置完执行nginx -t检查语法,再systemctl reload nginx。这样用户访问域名时,请求由Nginx接收,静态资源直接返回,动态请求转发给Gunicorn处理。
这里有一个高频坑:云服务器安全组只放行了80端口,但SELinux或防火墙没开放8000端口,导致curl 127.0.0.1:8000有反应、外网访问却超时。如果Nginx报502,最常见原因是Gunicorn没起来、或者绑定地址错了。建议先把Gunicorn临时前台启动调通,再做成systemd服务:
ini复制[Unit]
Description=gunicorn daemon
After=network.target
[Service]
User=你的系统用户名
Group=你的系统用户组
WorkingDirectory=/你的项目路径
ExecStart=/你的项目路径/venv/bin/gunicorn 项目名.wsgi:application --bind 127.0.0.1:8000
Restart=always
[Install]
WantedBy=multi-user.target
5.3 部署时别忘了数据库和日志这两件家务事
如果你本地用的是SQLite,部署到服务器上是继续用SQLite还是换MySQL,这个问题需要提前决定。本地测试数据不想丢,可以先把SQLite数据导出,再导入MySQL;如果嫌麻烦,就在服务器上重新走一遍migrate+createsuperuser,然后手工录入少量演示数据。
日志配置也是很多人忽略的事情。Django默认把日志打到控制台,你在systemd里很容易看到,但如果服务重启后日志丢失,后端排错将非常不便。建议把日志单独写到项目目录下的logs/django.log,并配置按大小切割:
python复制LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.handlers.RotatingFileHandler',
'filename': BASE_DIR / 'logs' / 'django.log',
'maxBytes': 5 * 1024 * 1024,
'backupCount': 5,
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': True,
},
},
}
需要提前创建logs目录,否则Django启动时会因为找不到目录报错。这些都是部署文档里不一定写得细,但实际生产一定会碰到的东西。
6. 整套资料拿到手后,怎么消化成自己的项目
6.1 推荐的“食用顺序”:先跑,再读,最后改
很多同学拿到一套源码,第一件事就是注册账号到处点一遍,觉得能跑就完事了。这套流程做完,答辩时被老师追问一个底层问题就接不上了。
我的建议是分三步走:
第一步,把项目跑通,配合部署文档看目录结构,尤其要关注settings.py、urls.py、核心models.py和views.py。第二步,任选一个完整功能链路,从路由到视图、模型、模板、表单追一遍。比如员工新增:用户在页面上填写信息,点击提交后,数据怎么进数据库,页面又是怎么反馈的。第三步,改一个和原有源码不同的地方,比如加一个批量导入、报表统计、或者把原本的admin页面改造成独立的表单页面。这时候你才真正开始掌握这套项目。
6.2 论文部分怎么用,答辩时才能言之有物
资料包里的“lw”指的是配套文档,里面一般包含需求分析、系统设计、数据库设计、功能实现、测试等章节。这部分不能直接上交。老师多年看下来,什么题目是哪一套模板,一眼就能认出来。
建议按这个思路重写重点章节:
- 系统设计部分,画出功能结构图和业务流程图。不要用花里胡哨的样式,把“管理员是谁,员工是谁,各自能做什么”画清楚就好。
- 数据库设计部分,把你模型中每张表的关键字段、外键关系完整写出来,别只贴截图。
- 功能实现部分,不要全文贴代码,挑一个核心模块讲解,标注重点代码并说明设计意图。比如我为什么会用
select_related,为什么员工外键要设置成PROTECT。 - 测试部分,不要只写“测试全部通过”,要写测试用例、输入数据、预期结果、实际结果。
答辩时老师最爱问的几个问题,也可以提前准备:Django的ORM是怎么防止SQL注入的?ORM的select_related和prefetch_related有什么区别?csrf_token的作用是什么?外键的on_delete有哪些选项,每个选项的含义是什么?这些问题如果平时没有认真看代码,是答不出来的。
6.3 有条件的话,加一两个低成本高显示的亮点功能
不管你是做毕设还是练手,把源码原样跑通只能算及格。想让自己的版本看起来“有自己的工作”,最推荐加这两个功能:
第一个是Excel批量导入员工信息。做一个上传按钮,后端用openpyxl读取Excel,逐行校验后写入数据库。批量导入看起来功能不大,但能非常明显提升一个管理系统的完整度。
第二个是简单的可视化统计。用页面上的图表插件展示每个部门的人数分布、当月考勤异常次数。Django视图里把聚合查询的结果算好,返回JSON给前端,整个过程不需要引入额外的重框架。
如果你完整经历一遍“数据导入 -> 数据查询 -> 导出报表”的闭环,再去答辩时,底气是完全不一样的。
我自己在实际操作这套系统时也踩过不少坑,所以最后分享两个小建议:一是尽量保证你手里的源码和Python版本配套,下载资料时先看requirements.txt,版本不对轻则警告重则白忙;二是定期把数据库做一次导出备份,无论是在本地调试还是云端部署,数据丢了,代码再好也等于零。把这套系统的业务链路走顺,把模型关系画清楚,把自己写进去两三个功能模块,这套“基于Python+Django的员工管理系统”才真正算是你的作品。
