Python+Django学生考勤管理系统:从数据表设计到签到统计完整实现
2026/9/15 20:11:52 网站建设 项目流程

简介:面向计算机专业毕业设计、课程作业及Python实战练习的学生,提供考勤管理系统完整项目资料,基于动态网页技术构建,包含前端页面、后端业务逻辑、数据库表设计与接口调用,可帮助学习者掌握从需求分析、代码实现到部署上线的完整流程。资源包共445个文件,压缩后大小约11.61MB,主要以Python程序文件、Vue组件、SVG图标、JavaScript、CSS样式、SQL脚本、Word文档、配置文件等类型呈现,其中源码经过严格调试可直接运行,并配有详细的安装、运行、初始化等批处理脚本。项目经导师指导并通过评审,评审分达98分,附带完整部署教程与设计论文,方便快速搭建环境、验证功能、撰写文档。目前已有178人学习下载,内容难度适中,适合需要完成毕设或系统学习项目开发的学生参考和复用。

1. 基于 Python 的学生考勤管理系统,毕设和实训项目都能直接落地

临近毕业季,学生考勤管理系统是 Python 方向出镜率最高的毕设选题之一。它的业务边界清晰:学生、班级、课程、签到签退、请假、迟到早退、缺勤统计,每一块都是标准的增删改查加统计报表,既能展示后端基本功,又方便答辩时现场演示。但这个题目也最容易踩坑:很多人把系统写成了“只有登录和列表”,答辩时被问倒;或者签到接口能重复提交,统计结果对不上账;还有人在环境配置上卡了两天,MySQL 装好了连不上、Django 迁移报错,最后仓促交付。

这篇博文不做空泛的架构推演,而是从技术选型、数据表设计、考勤状态判定、统计查询到答辩论文的对应关系,把一套可复现的实现方案拆开讲清楚。想用现成源码快速跑通的人,也能在最后拿到一份检查清单和调参思路。

2. 为什么选 Python + Django 组合做考勤系统:选型理由与数据表设计

2.1 学生考勤管理系统选 Django 还是 Flask

考勤管理系统这类 CRUD 为主的业务系统,我一般优先选 Django,而不是 Flask。理由很直接:Django 自带 ORM、Admin 后台、认证系统和表单处理,这些在项目里几乎全用得上。学生管理、课程管理、考勤记录,本质上就是对几张数据表做增删改查;Django Admin 甚至可以在一开始不写一行前端代码的情况下,把后台管理界面跑起来,这对快速搭建和中期检查都非常有利。

Flask 的优势是轻量、自由,但代价是你要自己拼 SQLAlchemy、Flask-Login、WTForms 这些组件。对毕设项目来说,自由度高不一定好,反而是框定好的结构更容易控制进度。

对比维度DjangoFlask
自带 ORM有,支持关系映射和迁移需另装 SQLAlchemy
管理后台自带 Admin,开箱即用需要插件或自研
用户认证内置 User 模型和权限需自行实现或扩展
学习成本中高,但功能齐备低,但组装成本高
适合作业/毕设演示适合,后台直接可展示适合,但自主拼装要求高

我见过不少拿 Flask 做考勤系统的同学,最后花在组装登录、权限、分页上的时间,远超业务逻辑本身。建议除非你有明确理由,比如要展示微服务拆分或纯手写 ORM,否则直接选 Django。

2.2 考勤管理系统的核心数据表设计

一个合格的考勤系统,表不要多,但关系要清楚。常见做法是设计四张核心表:学生表、课程表、考勤记录表,再加一张学生选课关联表(或者用 Django 的 ManyToManyField 实现)。

下面是 Django models 的写法,可以作为重新建模或对照源码的基准:

from django.db import models from django.contrib.auth.models import User class Student(models.Model): name = models.CharField(max_length=50, verbose_name="姓名") student_no = models.CharField(max_length=20, unique=True, verbose_name="学号") major = models.CharField(max_length=100, blank=True, verbose_name="专业") phone = models.CharField(max_length=11, blank=True, verbose_name="手机号") class Meta: verbose_name = "学生" verbose_name_plural = "学生" def __str__(self): return f"{self.student_no} {self.name}" class Course(models.Model): name = models.CharField(max_length=100, verbose_name="课程名") course_code = models.CharField(max_length=20, unique=True, verbose_name="课程编号") teacher = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="授课教师") students = models.ManyToManyField(Student, through=" Attendance", related_name="courses", blank=True) class Meta: verbose_name = "课程" verbose_name_plural = "课程" def __str__(self): return f"{self.course_code} {self.name}" class Attendance(models.Model): STATUS_CHOICES = ( ("present", "正常"), ("late", "迟到"), ("early", "早退"), ("absent", "缺勤"), ("leave", "请假"), ) student = models.ForeignKey(Student, on_delete=models.CASCADE, verbose_name="学生") course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name="课程") date = models.DateField(verbose_name="日期") check_in_time = models.TimeField(null=True, blank=True, verbose_name="签到时间") status = models.CharField(max_length=10, choices=STATUS_CHOICES, verbose_name="状态") class Meta: unique_together = ("student", "course", "date") verbose_name = "考勤记录" verbose_name_plural = "考勤记录" def __str__(self): return f"{self.student} {self.course} {self.date} {self.get_status_display()}"

重点说明:unique_together = ("student", "course", "date")是这条数据的核心约束。它的作用是保证同一个学生在同一天、同一门课只有一条考勤记录,从数据库层面防止重复签到。很多项目只在前端做了判重,结果并发请求一来,重复记录就进来了,统计自然会出错。

Attendance作为中间表承接StudentCourse的多对多关系,同时携带考勤状态和时间字段。check_in_time允许为空,因为请假和缺勤状态没有签到时间,强制非空会让数据插入变得别扭。

2.3 初始化数据与系统管理入口

建模之后,第一步不是写业务代码,而是把数据库建好、创建一个管理员账户。这是检查开发环境是否正常的最短路径:

pip install Django python manage.py makemigrations attendance python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000

makemigrations attendance只会为 attendance 这个 app 生成迁移文件,避免把其他无关 app 的变动混进来;migrate负责真正创建数据表。createsuperuser会交互式地要求输入用户名、邮箱和密码,用于后续登录 Admin 后台。

启动后浏览器访问http://127.0.0.1:8000/admin,就能进入管理界面。在admin.py里只需要三行注册代码,后台就有了针对学生的完整增删改查入口:

from django.contrib import admin from .models import Student, Course, Attendance admin.site.register(Student, admin.ModelAdmin) admin.site.register(Course, admin.ModelAdmin) admin.site.register(Attendance, admin.ModelAdmin)

到这里,系统骨架已经搭好了。接下来的重点是考勤业务逻辑,也就是签到、签退和状态判定的实现。

3. 签到与考勤状态判定:迟到、早退、缺勤的正确打开方式

3.1 考勤状态判定的业务规则设定

考勤系统最容易被低估的部分,是状态判定逻辑。常见误区是把“迟到”和“早退”写成两个 bool 字段,实际上一个时间段里就只能有一个状态。更合理的做法是基于签到时间和下课签退时间,推导出最终状态。

规则可以这样定义:

场景条件状态
未签到且未请假整节课无记录缺勤
课前或课后宽限期内签到签到时间 ≤ 上课时间 + 宽限分钟正常
超过宽限期签到签到时间 > 上课时间 + 宽限分钟迟到
下课前离开签退时间 < 下课时间早退
提前申请请假记录存在请假

“宽限期”是一个重要参数。如果严格按上课时间判定,任何一两分钟的延迟都会被记为迟到,这在真实场景中并不合理,也容易在答辩时被追问。常见做法是把宽限分钟数设为 10 到 15,并且放进配置文件,而不是写死。

3.2 用 Python 实现考勤状态判定函数

状态判定逻辑独立成一个小函数,可以方便测试和复用。下面是一个考虑完整度的实现:

from datetime import datetime, time def determine_attendance_status( check_in: time | None, check_out: time | None, course_start: time, course_end: time, grace_minutes: int = 15, has_leave: bool = False ) -> str: """根据签到签退时间和课程时间,返回考勤状态。 参数说明: check_in: 学生签到时间,None 表示未签到 check_out: 学生签退时间,None 表示未签退 course_start: 课程开始时间,如 time(8, 0) course_end: 课程结束时间,如 time(9, 40) grace_minutes: 宽限分钟数,默认 15 分钟 has_leave: 是否已获批请假 """ if has_leave: return "leave" if check_in is None: return "absent" grace_limit = datetime.combine(datetime.today(), course_start) + \ __import__("datetime").timedelta(minutes=grace_minutes) check_in_dt = datetime.combine(datetime.today(), check_in) if check_in_dt > grace_limit: return "late" if check_out is not None and check_out < course_end: return "early" return "present"

逻辑上有几点要注意。先判断请假和未签到,因为这两个情况的优先级最高;再做迟到判定;最后看是否早退。如果签到时间在宽限期内且签退时间不正常,会被判为早退。

grace_limit的计算方式说明了宽限期的核心逻辑:以课程开始时间加上固定分钟数作为门槛。datetime.combine用于把 date 和 time 拼成 datetime,只有 datetime 才支持加减 timedelta。直接对 time 对象做加法会报错,这是初学者最常见的坑。

3.3 通过 Django 视图实现签到接口

有了判定函数,把它接入签到接口。这个视图要完成三件事:确认是有效课程、检查是否已签到、写考勤记录并返回结果。

from django.http import JsonResponse from django.views.decorators.http import require_POST from django.views.decorators.csrf import csrf_exempt from .models import Student, Course, Attendance from datetime import datetime @require_POST @csrf_exempt def check_in(request): student_id = request.POST.get("student_id") course_id = request.POST.get("course_id") now = datetime.now().time() try: student = Student.objects.get(pk=student_id) course = Course.objects.get(pk=course_id) except (Student.DoesNotExist, Course.DoesNotExist): return JsonResponse({"code": 1, "msg": "学生或课程不存在"}, status=400) attendance, created = Attendance.objects.get_or_create( student=student, course=course, date=datetime.today(), defaults={"status": determine_attendance_status( check_in=now, check_out=None, course_start=course.start_time, # 需要 Course 模型增加开始时间字段 course_end=course.end_time, grace_minutes=settings.ATTENDANCE_GRACE_MINUTES, ), "check_in_time": now} ) if not created: return JsonResponse({"code": 2, "msg": "已签到,请勿重复操作"}, status=200) return JsonResponse({"code": 0, "msg": "签到成功", "status": attendance.status})

get_or_create是并发场景下的关键操作。它依据模型中的unique_together约束,在数据库层面避免重复创建记录。如果记录已存在,created为 False,视图直接返回提示信息。

注意course.start_timecourse.end_time需要 Course 模型有这两个字段。如果原表没设计,可以在迁移中加入:

# 在 Course models.py 中添加: start_time = models.TimeField(default=time(8, 0), verbose_name="开始时间") end_time = models.TimeField(default=time(9, 40), verbose_name="结束时间")

3.4 时间边界与配置参数:超时、时区和默认值

签到接口一定要设置超时,否则学生端网络慢会导致请求挂起,同一请求被前端重发,产生重复记录。Django 中可以用 session 或缓存做幂等,也可以在前端做按钮禁用,但最可靠的后端方案还是get_or_create配合唯一约束。

另一个高频坑是时区问题。Django 默认的USE_TZ = True会把时间按 UTC 存储,而中国时区是 UTC+8。如果直接datetime.now()取值,保存到数据库的时间会差 8 小时。正确做法是在settings.py中设置:

TIME_ZONE = "Asia/Shanghai" USE_TZ = False

USE_TZ = False表示不使用带时区的时间对象,datetime.now()直接取本地时间。如果项目坚持要USE_TZ = True,那 DJango 里要用django.utils.timezone.localtime()来转换,这一点在答辩描述项目时很容易被问到。

4. 出勤统计与报表查询:Django ORM 聚合与导出 Excel

4.1 用 annotate 统计学生出勤率

考勤管理的落脚点是统计报表。常见需求是“某学生当前课程出勤率”“某课程每日到课人数”。这类查询用 Django ORM 的聚合函数实现,清晰且不易出错。

from django.db.models import Count, Q from .models import Attendance, Student def student_attendance_rate(student_id: int, course_id: int) -> dict: stats = Attendance.objects.filter( student_id=student_id, course_id=course_id ).aggregate( total=Count("id"), present_count=Count("id", filter=Q(status="present")), late_count=Count("id", filter=Q(status="late")), leave_count=Count("id", filter=Q(status="leave")), ) total = stats["total"] # 出勤率计算:正常 + 迟到(迟到的本质是到课了) present = stats["present_count"] + stats["late_count"] rate = round(present / total * 100, 2) if total else 0.0 return {"total": total, "rate": rate, **stats}

Count("id", filter=Q(...))是 Django 2.0 之后支持的用法,它在一次查询内完成多条件计数,避免写多条 ORM 再拼结果。这里要特别说明一个统计口径的问题:迟到算不算出勤。严格意义上,迟到是“到课但未按时到”,所以出勤率的分子应该包含正常和迟到,缺勤和请假不进分子。很多粗糙的实现把“正常”当作出勤,导致迟到学生的数据失真,答辩时被问到会很难解释。

4.2 用 openpyxl 导出考勤报表

统计完还要能导出。Excel 导出是毕设项目的常见加分项,也常出现在中期检查或答辩演示中。用 openpyxl 写一个导出函数:

from openpyxl import Workbook from django.http import HttpResponse def export_attendance_report(course_id: int): wb = Workbook() ws = wb.active ws.title = "考勤报表" ws.append(["学号", "姓名", "日期", "签到时间", "状态"]) records = Attendance.objects.filter(course_id=course_id).select_related("student") for rec in records: ws.append([ rec.student.student_no, rec.student.name, rec.date.strftime("%Y-%m-%d"), rec.check_in_time.strftime("%H:%M:%S") if rec.check_in_time else "", rec.get_status_display(), ]) response = HttpResponse( content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) response["Content-Disposition"] = f"attachment; filename=attendance_{course_id}.xlsx" wb.save(response) return response

select_related("student")在这里不是可有可无的优化句。它通过 SQL 的 JOIN 一次性把关联的学生表数据查出来,避免循环中反复访问数据库产生 N+1 问题。导出几百行数据时可能感受不深,到几千行时速度差距非常明显。

get_status_display()是 Django model 的固有方法,返回选择字段的中文描述。比如status存储的是"present",该方法输出"正常"

4.3 统计查询常见的 3 个坑

第一个坑是 N+1 查询。上面代码中select_related专门处理ForeignKey;如果是多对多关系或ManyToManyField,需要换用prefetch_related。两者的区别是:前者用 JOIN,后者用两次查询再组装,不能混用。

第二个坑是聚合结果的类型。Count返回的是 Python 整数,Avg返回的是 Decimal 或 float。如果直接做除法或字符串拼接,容易踩类型错误。比如rate = present / total * 100,在 Python 3 中结果是 float,但如果 total 是 0,就会抛ZeroDivisionError,所以代码里必须加if total else 0.0的保护。

第三个坑是时区导致的日期偏移。学生签到时间是 23:50,UTC 存储后变成第二天 07:50,按date字段查询时会归入错误的一天。这个问题在上一章提到的USE_TZ = False可以规避,但如果接手的是现成项目,要先检查settings.py里的时区配置再写统计逻辑。

5. 从源码到答辩:快速演示的检查清单与论文对应技巧

5.1 拿到或写完系统后,30 分钟内完成演示准备

无论你是用现成源码还是自己写的系统,答辩前的演示路径应该固定且顺畅。先在本地跑通,再准备少量数据,最后演练两条核心业务链路:管理员录入学生和课程、学生完成签到并查看统计结果。

第一,依赖安装到位。项目根目录通常有requirements.txt,执行pip install -r requirements.txt安装全部依赖。如果网络环境不佳,可以分别安装 Django 和 openpyxl,这两个是核心依赖。

第二,数据库迁移。执行python manage.py migrate前,先确认settings.py中数据库配置和实际安装的数据库一致。如果用默认的 SQLite,不需要额外安装服务和配置账号;如果用 MySQL,要先确保 MySQL 服务已启动,并且配置了正确的用户名密码。

第三,创建超级管理员。createsuperuser创建的账号用于 Admin 后台。现场演示时建议提前建好账号和账号数据,不要在现场敲命令建学生。

第四,预置演示数据。把 5 到 10 条学生记录、3 门课程、若干条不同的考勤状态(正常、迟到、请假、缺勤都要有)提前录好。这样演示报表时,表格里不会全是空数据。

5.2 论文里数据表设计和 ER 图这样写才不被问倒

如果这篇博文正好对应你的毕业论文,那么论文的技术部分重点不是贴代码,而是讲清楚数据库设计决策和考勤判定规则。

ER 图要画出的关系至少包括:学生和课程之间的多对多(通过考勤记录表连接)、课程和教师之间的一对多、学生和考勤记录之间的一对多。这是三张表之间最核心的关系,不要省。答辩提问环节,老师大概率会指着 ER 图追问“为什么中间表要有唯一约束”,此时可以用第 2 章中unique_together的解释来回答:防止同一个学生在同一门课的同一日期出现多条记录,保证统计口径一致。

考勤规则部分,建议用一张判定表呈现,这比纯文字描述直观得多。把第 3 章中的规则表格直接放进论文,再配合一小段判定函数的伪代码或 Python 代码。注意伪代码不要长篇大论,能表达条件分支即可。

5.3 一个加分技巧:把考勤统计做成前端可调用的接口

很多毕设项目止步于 Admin 后台,缺少一个面向普通用户的界面或接口。如果你有余力,做一个简单的 JSON 接口,用于查询某学生的出勤率,会让项目完整性上升一个档次。

接口可以这样写:

from django.http import JsonResponse def student_rate_api(request, student_id): course_id = request.GET.get("course_id") data = student_attendance_rate(student_id, course_id) return JsonResponse(data)

前端只要配合一个下拉框和表格,就能实现“选课程 -> 看出勤率”的交互。不需要复杂的前端框架,一个原生 HTML 页面加fetch即可。这个接口的价值在于:它把后端逻辑暴露为可测试的服务,明显区别于单纯依赖 Django Admin 填表的方案,也方便在演示时用浏览器直接访问http://localhost:8000/api/attendance/1/?course_id=1验证结果。

时间有余的话,可以把export_attendance_report也绑定成一个按钮入口,点击后下载 Excel,这几乎是答辩时最能直观展示项目完成度的操作。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询