又到一年毕设季,私信和评论区里被问得最多的问题,永远绕不开一句话:"学长,基于 Python 的考勤系统到底怎么做?"每年我都要回答很多遍类似的问题,今年干脆把这套 Django 考勤系统的完整设计与实现思路写成一篇长文,从选题逻辑、数据库设计、签到签退核心逻辑、请假审批流程,再到环境配置、部署运行、答辩准备,一条线全讲透。这篇文章适合三类人看:正在纠结毕设选题的应届生、想用 Django 做第一个完整 Web 项目的新手,以及单纯想在本地搭一套考勤工具练手的同学。读完你至少能搞清楚一个问题:一套看起来平平无奇的考勤系统,背后到底藏着多少门道。
1. 项目概述与选题思路
1.1 考勤系统为什么是毕设的"安全牌"
每年带毕设我都会给学生一个建议:选题别贪大,也别太偏。所谓"大",就是那种上来就要做人工智能、分布式高并发、微服务架构的题目,以本科阶段的水平和时间投入,最后大概率做出一个空壳子;所谓"偏",就是选一个导师不熟悉、你自己也说不清楚应用场景的冷门方向,答辩的时候很难自圆其说。考勤系统恰恰是中间的"安全牌"。
为什么这么说?第一,业务场景足够清晰。考勤是每个人都接触过的事情,签到、签退、请假、统计出勤,不需要额外解释业务背景,评委一看就懂。第二,技术覆盖面恰好踩在 Django 的核心能力上:用户认证、ORM 建模、视图编写、模板渲染、表单处理、权限控制,全部能练到,但又不会要求你掌握分布式、消息队列这种超纲内容。第三,可扩展的口子很多,答辩时你能讲的东西远超一个普通管理系统。
这套系统本质上是一个典型的 Web 信息管理系统:前端页面负责交互,后端 Django 负责业务逻辑和数据持久化,数据库存用户、部门、打卡记录、请假审批单。把这个流程吃透了,以后换一个"图书馆管理系统""实验室设备管理系统",套路完全一样,只是换了一批表和业务规则而已。
1.2 技术选型:Django 到底赢在哪里
Python 做 Web 开发可选框架不少,Flask、FastAPI 都是很好的工具,但作为毕设题目,我坚持推荐 Django,原因很实在。
Django 自带的东西太多了。用户认证系统(django.contrib.auth)开箱即用,登录、登出、Session 管理、密码哈希全都帮你处理好了,不用自己写 Session 和 Cookie 的底层逻辑;Admin 后台更是神器,模型建好之后自动生成管理界面,管理员可以直接在后台维护部门信息、查看打卡记录、审批请假单,这在毕设演示阶段能省下大把开发时间。还有 Django ORM,写 Python 代码操作数据库,不用手写 SQL,对很多数据库基础薄弱的同学来说,这是最大的福音。
再就是生态和资料。Django 的文档非常完善,遇到问题搜索一下基本都有答案,社区里关于考勤系统、管理系统的案例一抓一大把。作为毕设项目,可参考的资料越多,你翻车之后爬出来的成本就越低。数据库我用的是 SQLite 起步,零配置,一个文件搞定,适合开发调试;如果想体现一点"工程能力",后期切换到 MySQL 也就改一个DATABASES配置的事,这个我会在部署章节详细说。
1.3 系统角色划分与核心业务流程
考勤系统的角色不需要设计得太复杂,两到三个角色足够撑起整个业务闭环。我采用的是"管理员 + 员工"双角色模型:管理员负责部门管理、员工账号管理、查看所有考勤记录、审批请假申请;员工负责日常签到、签退、提交请假申请、查看自己的考勤记录和个人信息。
核心业务流程也很好梳理,总共就四条线。第一,员工每天上班打卡签到、下班打卡签退,系统记录时间并自动判定是否迟到早退。第二,员工有事需要请假,提交请假单,管理员审批,审批通过后请假时段不计入缺勤。第三,系统根据每天的打卡记录和请假记录,按月汇总每个员工的出勤天数、迟到次数、早退次数、请假天数和缺勤天数。第四,管理员可以在后台做基础的部门管理、员工信息管理。
看清楚了吗?这个流程就是把现实世界的考勤制度"翻译"成业务逻辑,每一步都能对应到一个具体的功能模块。后面所有的代码和设计,都是围绕这四条业务线展开的。
2. 系统架构与数据模型设计
2.1 功能模块全景图
整个项目我拆成了五个模块,每个模块对应一个 Django app 或者一个逻辑分组,这样代码结构清晰,答辩时也好讲。
- 用户认证模块:基于 Django 自带的 auth 体系实现注册、登录、登出,登录后根据用户角色跳转到不同页面。
- 考勤打卡模块:签到、签退、打卡记录查询、迟到早退判定、防重复打卡。
- 请假管理模块:请假申请、审批、历史记录、状态流转(待审批/已通过/已驳回)。
- 考勤统计模块:按个人和按部门的月度考勤汇总,出勤率、请假率、迟到次数统计。
- 系统管理模块:部门管理、员工账号管理,可以直接复用 Django Admin 再做一些定制。
每个模块都不是孤立的。员工打卡后产生考勤记录,请假审批通过后会影响考勤统计结果,部门信息挂在用户上用于分组统计。所以说数据模型设计是整个项目的地基,地基打不好,后面写业务逻辑的时候处处别扭。
2.2 models.py 数据表设计详解
我直接贴一份核心模型的简化代码,然后把每个字段的设计理由讲清楚。这是整套系统最值得花时间琢磨的部分。
from django.contrib.auth.models import AbstractUser from django.db import models class Department(models.Model): """部门表""" name = models.CharField('部门名称', max_length=50, unique=True) code = models.CharField('部门编码', max_length=20, unique=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) def __str__(self): return self.name class User(AbstractUser): """自定义用户表,继承 Django 自带用户模型""" department = models.ForeignKey( Department, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='所属部门' ) phone = models.CharField('手机号', max_length=11, blank=True) avatar = models.ImageField('头像', upload_to='avatar/', blank=True) class Meta: verbose_name = '用户' verbose_name_plural = verbose_name先解释为什么继承AbstractUser而不是直接用默认的User或者自己从零写一张用户表。毕设项目几乎必然要扩展用户字段,至少你需要给用户挂一个部门外键,所以自定义用户模型是标准动作。继承AbstractUser的好处是,Django 自带的用户名、密码、邮箱、权限字段全部保留,你只需要添加业务字段,省时省力。注意有一个关键点:如果你打算自定义用户模型,必须在第一次执行makemigrations迁移之前就配置好AUTH_USER_MODEL,否则中途更换用户模型会引发一堆难以收拾的迁移错误。这个坑我见过好几个人踩,后面第五章会细说。
部门表为什么单独建?因为用户和部门是多对一的关系,一个部门有多个员工,一个员工只属于一个部门。把部门独立成表,以后改部门名称、加部门负责人字段都很方便,不会在用户表里产生冗余数据。部门编码我加了unique=True,这是为了以后可能接打卡机、门禁系统做外部系统对接时有一个稳定的业务标识。
接着是考勤记录表和请假申请表。这两张表才是业务核心所在。
class AttendanceRecord(models.Model): """考勤打卡记录表""" user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='员工') date = models.DateField('出勤日期') check_in_time = models.DateTimeField('签到时间', null=True, blank=True) check_out_time = models.DateTimeField('签退时间', null=True, blank=True) is_late = models.BooleanField('是否迟到', default=False) is_leave_early = models.BooleanField('是否早退', default=False) status = models.CharField( '出勤状态', max_length=10, choices=[ ('normal', '正常'), ('late', '迟到'), ('leave_early', '早退'), ('absent', '缺勤'), ], default='normal' ) remark = models.CharField('备注', max_length=200, blank=True) class Meta: # 同一个人同一天只能有一条考勤记录 unique_together = ('user', 'date') verbose_name = '考勤记录' verbose_name_plural = verbose_name class LeaveRequest(models.Model): """请假申请表""" user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='申请人') start_time = models.DateTimeField('开始时间') end_time = models.DateTimeField('结束时间') reason = models.TextField('请假事由') status = models.CharField( '审批状态', max_length=10, choices=[ ('pending', '待审批'), ('approved', '已通过'), ('rejected', '已驳回'), ], default='pending' ) apply_time = models.DateTimeField('申请时间', auto_now_add=True) approve_time = models.DateTimeField('审批时间', null=True, blank=True) approver = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_leaves', verbose_name='审批人' )考勤记录表有几个设计细节值得强调。
第一,unique_together = ('user', 'date'),这是在数据库层面做硬约束:同一个人同一天最多一条考勤记录。有了这个约束,后面写签到签退逻辑时就不用担心并发产生重复记录,这个约束是防重复打卡的最后一道防线。
第二,签到时间和签退时间我用了DateTimeField,出勤日期单独用了DateField。为什么要拆开?"出勤日期"是一个业务概念,它可能跟自然日不是完全一致的。比如一个员工上夜班,晚上十点上班、次日凌晨两点下班,那这次班次归属到哪一天,就需要单独定义。这个问题我在第五章展开讲,这里先把"日期归属"这个口子留出来。
第三,迟到、早退没有直接用布尔字段去覆盖所有情况,而是保留了一个更完整的status字段。为什么不只用两个布尔值?因为业务上出勤状态是互斥的:一个人当天的状态要么正常、要么迟到、要么早退、要么缺勤。用选择题字段表达互斥状态,比你同时维护is_late和is_leave_early两个字段要清晰得多。我代码里两个布尔字段还留着,是为了统计时好做分组,但真正的状态表达以status为准。
请假表里,start_time和end_time是DateTimeField而不是DateField,因为请假通常精确到半天甚至小时;approver用了related_name='approved_leaves',否则 Django 会默认生成一个leave_request_set,在反向查询时语义会很奇怪,显式命名是提高代码可读性的一个小习惯。
2.3 关系设计的几个关键决策
外键的on_delete行为,是我每次讲模型设计都要重点强调的点。很多新手不管三七二十一全用CASCADE,结果删一个部门把整个部门的员工和考勤记录全删了,数据灰飞烟灭。这里要分场景取舍。
- 考勤记录和请假记录对用户:用
CASCADE。用户删了,他的打卡记录和请假单也没有保留意义了,级联删除省得你写一堆孤儿数据清理代码。 - 用户对部门:用
SET_NULL。部门解散时,员工账号应该保留,最多把部门置空,绝不能跟着部门一起消失。 - 请假单对审批人:用
SET_NULL。审批人账号删除了,历史请假单还要保留,审批人字段置空即可。
这三个on_delete选型就是典型的数据安全思考:什么数据是核心资产不能丢,什么数据是附属记录可以跟着主键一起删。答辩时把这个逻辑讲出来,导师会觉得你真的在做设计而不是在抄代码。
还有一个决策是:考勤记录要不要单独存"出勤日期"字段,而不是直接取check_in_time的日期部分?我的答案是要。一方面是为了上面说的跨天班次归属问题,另一方面是让统计查询变得简单:AttendanceRecord.objects.filter(date__year=2026, date__month=5)这种查询一看就懂,不需要对时间字段做转换。为了查询效率和代码可读性,牺牲一点存储冗余是完全值得的。
3. 核心功能实现与代码讲解
3.1 登录认证与权限控制
用户认证直接使用 Django 自带的django.contrib.auth,我只需要写一个视图处理登录请求,一个视图执行登出,再写一个判断当前用户角色的辅助方法。
from django.contrib.auth import authenticate, login, logout 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 is not None: login(request, user) # 根据角色跳转 if user.is_superuser: return redirect('admin_dashboard') return redirect('user_dashboard') return render(request, 'attendance/login.html', {'error': '用户名或密码错误'}) return render(request, 'attendance/login.html') def logout_view(request): logout(request) return redirect('login')authenticate函数会帮你校验用户名和密码,密码在数据库里存的是加盐哈希,不是明文,这块完全不用自己操心。登录成功后调用login写入 Session,后续请求通过request.user就能拿到当前登录用户。
权限控制我用的是装饰器加判断。Django 提供了@login_required装饰器,未登录用户访问受保护页面会被重定向到登录页。角色区分就简单判断一下is_superuser。如果想让角色体系更正规,可以在 User 上加一个role字段,用choices限定取值,然后在视图里封装一个require_role('admin')装饰器。不过对考勤系统来说,超管标志加上普通用户足以覆盖业务场景,不推荐过度设计。
3.2 签到签退的业务逻辑
签到签退是整套系统里最考细节的模块,业务上要考虑的东西非常多。先看签到的一段核心逻辑。
from datetime import datetime from django.shortcuts import get_object_or_404 from django.utils import timezone from .models import AttendanceRecord, LeaveRequest def check_in(request): if not request.user.is_authenticated: return redirect('login') user = request.user today = timezone.localdate() # 防重复签到:当天已有记录且已签到 record, created = AttendanceRecord.objects.get_or_create( user=user, date=today, defaults={'check_in_time': timezone.now()} ) if not created or record.check_in_time is not None: return render(request, 'attendance/message.html', {'msg': '今天已经签到过了'}) # 判定迟到:假设上午 9 点为上班时间 work_start = timezone.now().replace(hour=9, minute=0, second=0, microsecond=0) if timezone.now() > work_start: record.check_in_time = timezone.now() record.is_late = True record.status = 'late' record.save() return render(request, 'attendance/message.html', {'msg': '签到成功,但已迟到'}) record.check_in_time = timezone.now() record.status = 'normal' record.save() return render(request, 'attendance/message.html', {'msg': '签到成功'})这段代码用了get_or_create来防重复,这是第一次打卡时最优雅的写法:如果当天记录不存在,创建一条;如果存在,直接拿到已有记录。然后判断check_in_time是否为空。这里有同学会问:为什么不直接用unique_together来挡重复?因为unique_together是数据库层面的兜底,但如果你不去查询就直接create,数据库会抛IntegrityError,你还要去捕获异常,体验很差。用get_or_create加空值判断,逻辑上更顺。
迟到判定的代码里,我把 9 点硬编码了。真实项目里上班时间应该是可配置的,比如放在系统参数表里,管理员改配置不影响代码。毕设阶段可以先用常量,但答辩时最好主动提一句"这个时间应该做成配置项",展示你的扩展思维。
签退逻辑类似,只是要额外判断一个边界:不能还没签到就签退。也就是说,record.check_in_time必须非空才能执行签退。还有,签退不允许覆盖已存在的签退时间,同样要用空值判断挡掉。
3.3 请假审批的状态流转
请假审批是一个典型的工作流场景,但只有一个审批层级,用状态字段就能表达。核心状态有三种:待审批、已通过、已驳回。状态流转方向是:待审批 -> 已通过,待审批 -> 已驳回,已驳回可以重新申请(重新申请就是创建一条新记录,而不是改状态)。
def submit_leave(request): if request.method == 'POST': LeaveRequest.objects.create( user=request.user, start_time=request.POST.get('start_time'), end_time=request.POST.get('end_time'), reason=request.POST.get('reason'), ) return redirect('my_leaves') return render(request, 'attendance/submit_leave.html') def approve_leave(request, leave_id): # 只允许管理员执行审批操作 if not request.user.is_superuser: return redirect('dashboard') leave = get_object_or_404(LeaveRequest, pk=leave_id) if request.method == 'POST': action = request.POST.get('action') if action == 'approve': leave.status = 'approved' leave.approver = request.user leave.approve_time = timezone.now() leave.save() elif action == 'reject': leave.status = 'rejected' leave.approver = request.user leave.approve_time = timezone.now() leave.save() return redirect('leave_list') return render(request, 'attendance/approve_leave.html', {'leave': leave})判断请假是否与考勤冲突,有一点需要提前考虑:如果员工某一天上午请假、下午正常上班,那这一天的出勤怎么算?我采用的规则是:请假时长覆盖某天的工作时段,则该天不算缺勤,但也不计为正常出勤,统计时归入"请假"分类。如果请假只覆盖了部分时段,规则会更复杂,毕设阶段可以简化成"当天有请假记录就不做迟到早退判定,出勤状态记为'请假'"。
请假审批这里有一个新手经常犯的错误:直接在模板里把管理员操作和普通用户操作混在一起。演示的时候没问题,但审批逻辑放出来就意味着普通用户也能访问approve_leave视图。Django 里get_object_or_404只管找对象,不管权限,is_superuser的判断必须显式写在视图里,这个判断哪怕丑,也不能省。
3.4 考勤统计的聚合查询
考勤统计是最能体现 Django ORM 水平的部分。月度统计我用的核心工具是aggregate和Count、Sum,一条查询搞定以前用存储过程才能做的事情。
from django.db.models import Count, Sum def monthly_statistics(request, year, month): records = AttendanceRecord.objects.filter( date__year=year, date__month=month, user=request.user ) stats = records.aggregate( total=Count('id'), late_count=Count('id', filter=Q(status='late')), leave_early_count=Count('id', filter=Q(status='leave_early')), absent_count=Count('id', filter=Q(status='absent')), ) return render(request, 'attendance/statistics.html', {'stats': stats})这里用到了Count的filter参数,这是 Django 2.0 以后支持的特性,可以在一个聚合查询里同时统计多个条件,不用写多个查询再拼结果。如果你的 Django 版本比较老,就得用Case/When表达式来实现同样的效果。
部门维度统计稍微复杂一点,需要从用户表连到考勤记录表再按部门分组:
from django.db.models import Count def department_statistics(request, year, month): stats = ( AttendanceRecord.objects .filter(date__year=year, date__month=month) .values('user__department__name') # 跨表分组 .annotate( total=Count('id'), late_count=Count('id', filter=Q(status='late')), ) .order_by('user__department__name') ) return render(request, 'attendance/dept_statistics.html', {'stats': stats})values加annotate是 Django ORM 做分组统计的标准姿势。这里踩过坑的同学会知道,values里面的字段名必须是 ORM 关系的双下划线写法,比如user__department__name,如果写成 SQL 风格的点号就会报错。这个错误信息有时候不太直观,我记得到时候搜索的时候就搜FieldError Cannot resolve keyword,基本都能查到解决方案。
统计这里还要注意一个业务口径问题:absent_count到底怎么算?考勤记录表里只有员工打卡才会产生记录,缺勤的人当天根本没有记录,直接Count(status='absent')统计出来永远是零。所以正确的缺勤统计逻辑应该是:先算出应出勤天数(当月工作日减去请假天数),再减去实际有打卡记录的天数,差值就是缺勤天数。这个口径问题很重要,答辩时如果你能主动讲出来,说明你真的理解了数据模型和业务之间的关系。
4. 环境准备与项目运行
4.1 Python 与虚拟环境配置
拿到源码第一件事是配环境,很多同学跑不起来项目不是代码的问题,是环境的问题。先说版本,Django 3.2 LTS 版本建议搭配 Python 3.8 到 3.10;Django 4.x 建议 Python 3.10 以上。我个人推荐用 Python 3.10 加 Django 4.1 的组合,稳定性和生态兼容性都比较好。
Python 装好后,一定要建虚拟环境,不要在全局环境里直接pip install。虚拟环境的好处是隔离依赖,不同项目之间互不干扰,尤其是你电脑上还有别的 Python 项目时,没有虚拟环境的后果就是,今天装个包把另一个项目的依赖版本搞坏了,到时候哭都来不及。
# 创建虚拟环境 python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS / Linux) source venv/bin/activate # 安装依赖 pip install django==4.1 pillow # 生成依赖清单(方便别人复现环境) pip freeze > requirements.txtpillow是 Django 处理图片上传必需的库,项目里如果员工头像用ImageField,离开它你的makemigrations会在校验模型时报错,提示你需要安装 Pillow 库。这个报错很常见,属于环境问题,提前装好能省一次搜索。
4.2 项目初始化与数据库迁移
如果你是从零开始建项目,顺序是这样的:
# 创建项目 django-admin startproject attendance_project # 进入项目目录并创建应用 cd attendance_project python manage.py startapp attendance python manage.py startapp user_auth # 把 app 注册到 settings.py 的 INSTALLED_APPS 中 # 配置完模型后,生成迁移文件并执行迁移 python manage.py makemigrations python manage.py migrate # 创建超级管理员 python manage.py createsuperuser如果你拿到的是现成源码,需要执行的步骤就少很多。但有一个关键动作不能省:检查settings.py里的SECRET_KEY。源码包里一般会带着一个固定的SECRET_KEY,这本来只是用于加密签名,但如果你要部署到公网服务器,必须换掉它,否则存在安全风险。本地开发无所谓,养成习惯就好。
makemigrations和migrate的区别,不少新手搞不清。makemigrations是根据你写的模型生成迁移脚本,它只是生成了一个记录文件,并没有真正改数据库;migrate才是把迁移脚本应用到数据库,真正建表。所以流程一定是先makemigrations再migrate,顺序反了或者漏了,数据库跟模型就对不上。
还有一个常见坑:迁移完数据库后,你发现模型改了一个字段,重新执行makemigrations时报 "Field 'xxx' doesn't have a default value"。这个通常是因为你给已有数据的表新增了一个非空字段。解决办法是给字段加default或者null=True,然后再迁移。
4.3 静态文件、时区等基础配置
settings.py里的配置,有几个地方需要特别留意。第一个是INSTALLED_APPS,除了你自己的 app,django.contrib.admin、django.contrib.auth是必须保留的基础应用,删了系统直接崩。
第二个是关键的语言和时区配置:
LANGUAGE_CODE = 'zh-hans' # 后台显示中文 TIME_ZONE = 'Asia/Shanghai' USE_TZ = TrueUSE_TZ = True意味着 Django 在数据库里存的是 UTC 时间,模板渲染时自动转成TIME_ZONE指定的时区。但如果你的业务代码里直接用datetime.now()而不是timezone.now(),就会拿到本地时间,跟数据库里的 UTC 时间混淆,统计报表时间就会乱掉。所以写代码的时候一律用timezone.now(),这个习惯要从第一天就养成。
第三个是静态文件配置。开发阶段跑runserver,Django 会自动处理静态文件,但前提是你配置好STATICFILES_DIRS:
STATIC_URL = 'static/' STATICFILES_DIRS = [ BASE_DIR / 'static', ]注意STATIC_URL和STATICFILES_DIRS是两回事,一个是 URL 前缀,一个是本地目录路径,不要混为一谈。如果你静态文件还是 404,最大的可能就是你只配了STATIC_URL,没配STATICFILES_DIRS。
5. 常见问题与排查技巧实录
5.1 时间差 8 小时的经典问题
这是 Django 新手必踩的坑,几乎每个用 Django 做项目的同学都会遇到一次。现象是:你在数据库里看到的时间是正确的北京时间,但页面上一显示就少了 8 小时;或者反过来,明明刚签到,显示的时间却比当前时间快了 8 小时。
根因就是USE_TZ和TIME_ZONE的组合。USE_TZ=True时,Django 存储的时间统一转成 UTC,模板渲染时根据TIME_ZONE设置再转回本地时间。如果你在代码里混用了datetime.now(),它返回的是操作系统本地时间,Django 会以为你存的是 UTC 时间,显示的时候再转一次,就造成了 8 小时偏移。
解决办法很简单:所有取当前时间的地方都用django.utils.timezone.now(),所有日期字段都用 Django 的DateTimeField自动处理时区转换。还有一个隐蔽的小坑:如果你用filter(check_in_time__date=today)这种方式按日期过滤,要注意today必须用timezone.localdate()拿,而不是date.today(),否则在 UTC 和北京时间切换的临界时刻会出现查询不到记录的问题。
5.2 静态文件加载失败
开发阶段跑起来页面没有样式,控制台一堆 404,基本都是静态文件路径配置出了问题。最常见的错误是把STATIC_URL写成了本地路径C:/...,正确的是填 URL 路径,比如/static/。
还有一个容易被忽略的点:如果你的项目里用到了图片上传功能,比如用户头像,那还需要配置MEDIA_URL和MEDIA_ROOT,并且在urls.py里加上静态服务:
from django.conf import settings from django.conf.urls.static import static urlpatterns = [...] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)这段代码只在DEBUG=True时生效,生产环境下需要由 Nginx 这类 Web 服务器来提供静态文件服务,Django 本身不擅长干这个。你如果了解这个原理,答辩的时候解释一下为什么开发环境能显示图片、上线之后却显示不了,会是一个不错的加分项。
5.3 迁移文件冲突与误删
迁移文件的坑比想象中多。我见过最惨的例子是:项目已经删掉了某个表的迁移文件,直接重新migrate,结果数据库里已经有这个表了,报Table already exists。这种错误一旦出现,处理起来很麻烦,要么手动DROP TABLE,要么用migrate --fake。
migrate --fake是一个危险但有用的命令,它的作用是"假装执行了迁移",把迁移记录写入 Django 的迁移记录表,但不去实际执行 SQL。什么场景下用?比如你的数据库表已经手工建好了,只需要让 Django 知道"这个迁移已经完成了",就用--fake。但如果你根本不了解数据库当前状态,千万不要乱用这个命令,它会让你的迁移记录跟实际表结构脱节,后续所有迁移都会异常。
我的建议很简单:迁移文件是项目的一部分,不要轻易手动删除。如果模型改错了,不要急着删文件,先把模型改正确,再执行makemigrations,Django 会生成新的迁移来修正表结构,不会报错。删除迁移文件的唯一安全时机是项目还没部署、数据库还没建、你可以把整个数据库文件和迁移目录一起清空重来。
5.4 打卡周期归属的边界处理
很多考勤系统上线后遇到的第一个业务问题就是:夜班员工怎么算。晚上 22:00 上班,次日凌晨 02:00 下班,这一天的工作时间横跨了两个自然日,按照签到时间来算"出勤日期",就会得出完全错误的结论。
我在模型设计时单独保存了date字段,就是为了处理这个问题。具体的规则可以这样设计:系统以"签到时刻所属的工作日"作为当天的出勤归属。比如夜班员工 22:00 打卡,这个时间点属于当天,所以出勤日期记为当天;第二天凌晨两点签退时,只需要把check_out_time写到同一条记录的签退字段里,不再生成新的记录。
签到逻辑里判断"当天是否已签到",也要注意用date字段而不是check_in_time的日期。比如夜班员工凌晨 02:00 签退后,如果系统用自然日判断,会认为"今天"还没有签到,实际上他今天早上才刚下班。正确的做法是用AttendanceRecord.objects.filter(user=user, date=today)去查,today取的是业务上定义的出勤归属日。这个案例在演示时讲出来,会让人觉得你真的在认真考虑边界情况。
6. 答辩讲解与项目扩展方向
6.1 如何在答辩中讲明白这套系统
代码写完了,项目跑起来了,答辩怎么讲?我的经验是:不要从一行行代码讲起,要从业务逻辑讲起。评委最想听的是你如何把一个现实问题转换成技术方案,而不是你用了几层循环。
一个比较稳妥的讲述顺序是:先讲清楚考勤系统是什么、解决什么问题;然后讲系统拆成了哪些模块,每个模块的职责是什么;接着重点讲一两个有技术含量的点,比如自定义用户模型的设计、考勤统计的聚合查询、防重复打卡的并发处理思路;最后现场演示一遍核心流程,从登录到打卡到审批到统计,一气呵成。
演示的时候有几个加分细节值得记住。第一,提前准备好测试账号,两个角色各一个,别在现场现注册,万一网络慢出幺蛾子。第二,演示签到的时候故意演示一次"重复签到",页面提示"今天已经签到过了",同时解释数据库层面的unique_together约束如何兜底,这个细节能证明你的设计深度。第三,展示统计页面时,把不同状态的数字指着念一遍,说明每个数字是怎么算出来的,口径清晰。
还有一个看起来小但很重要的点:把requirements.txt、README、数据库设计说明文档放在项目根目录,答辩时老师问"这个项目怎么部署",你直接指文档,体现工程素养。
6.2 从毕设到真实项目的升级路径
考勤系统做完,如果你想让它从"毕设"变成"能用的工具",有几个方向可以扩展。
第一,接入企业微信或钉钉的打卡接口。现在很多公司不用单独的系统考勤,直接用钉钉的定位打卡,把钉钉的数据同步过来,再做统计分析,这个方向很贴近真实需求。第二,增加考勤异常申诉流程。员工对迟到判定有异议,可以提交申诉,管理员复核后修改状态,这就引入了第二条审批线。第三,把打卡时间做成可配置的规则引擎。不同部门上班时间不同,打卡规则也不同,做成一张"考勤规则表",用JSONField存规则,界面配置完成后台解析。第四,前端换成 Vue 或 React,Django 用 REST Framework 提供 API,这就是前后端分离的架构,含金量立刻提升一个档次。
每个扩展方向都能单独写成一部分内容,答辩时你只要说"我留了哪些接口、下一步打算怎么做",就已经高于平均水平了。但如果你的进度紧张,我还是建议先保证核心功能的稳定和文档完善,再考虑这些锦上添花的部分。
做这套项目我最大的体会是:考勤系统的难点从来不在代码量,而在对业务规则的深刻理解。一个看似简单的"签到"动作,背后牵扯到防重复、时区、跨天归属、异常判定、统计口径这么多门道,每一处都是真实项目里会遇到的细节。把这些细节一个个想清楚、做扎实,比堆功能有意义得多。如果你正想拿这套思路做自己的考勤系统,我的建议是:先把数据模型设计吃透,把模型建对,业务逻辑自然就顺了;反过来,模型设计一塌糊涂,后面写的每一个视图都会让你感觉像在补救。希望这篇长文能帮你少踩几个坑,顺利把项目做出来。