简介:一套基于Python开发的高校学生违纪信息管理系统完整项目包,面向教务管理人员、辅导员以及有Python Web开发需求的学习者,解决违纪事件记录分散、查询统计繁琐、处理痕迹难追踪等问题。压缩包共391个文件、约16.5MB,结构清晰:内含84个py源码与76个pyc编译文件,对应系统后端逻辑;39个html、14个js、10个css等构成前端展示层,配合jpg、png等图片素材;同时携带sqlite3数据库文件,便于直接运行调试。系统功能覆盖违纪数据录入与条件查询、违规分类统计、警告与处罚记录、报表生成导出、分级权限控制及预警提醒,前端采用Bootstrap等框架增强易用性。已有124人学习下载,适合希望快速搭建信息化管理后台或需要完整源码作为课设、毕设参考的用户。
1. 高校学生违纪信息管理系统:这套 Python 源码解决的不只是登记问题
“基于 Python 的高校学生违纪信息管理系统”光看标题像课程设计,但把 zip 包拆完你会发现,它走的是 Django + Bootstrap 的老实路线:教务老师在浏览器里录入违纪记录,按姓名、违纪类型、时间范围查数据,后台自动按班级统计作弊、旷课、迟到的次数,期末一键导出 Excel 和 PDF 报表。这套工程里最值得看的不是页面好不好看,而是数据模型怎么设计、权限怎么划分、报表怎么导出——这三块直接决定了老师愿不愿意用、数据能不能沉淀下来。适合两类人:一类是正在学 Python Web 开发、想找个完整闭环练手的学生;另一类是校内确实想把违纪登记从 Excel 搬到网页上的管理人员。它不是大厂架构,但能跑、能改、能二次开发。
2. 从 Bootstrap 静态页到 Django 后端:违纪系统的技术栈与数据模型怎么选
先讲选型。源码包里那一串前端资源文件——bootstrap.min.css、animate.css、font-awesome.min.css、responsive.css、et-line.css、nivoslider.css、magnific-popup.css、owl.carousel.css、meanmenu.css——基本能判断出它用的是 Bootstrap 那一代模板,后端走 Django,数据库用 SQLite 起步。做违纪管理这种强权限、强表单、强统计的系统,这个组合不是随便选的,背后有明确的理由。
2.1 为什么是 Django + Bootstrap,而不是 Flask + Vue
这套系统要管三类角色:管理员看全部数据,教师只能看本班学生,学生只能看自己的记录。Django 自带 User 模型、Group 分组、Permission 权限装饰器和 Admin 后台,等于把最难写的用户体系和后台管理界面白送给你。Flask 不是不能做,但登录、会话、权限装饰器全得自己搭,工作量翻一倍不止。前端的 Bootstrap 则负责把录入表单、查询表格、统计图表这些页面组件快速拼起来,不需要前端工程化那套 Node 环境,对部署条件非常友好。
我拆这套源码时特别核对过静态资源文件,它们各自的分工很清楚,这决定了前端页面能撑起哪些交互:
| 文件 | 作用 |
|---|---|
| bootstrap.min.css | 栅格布局与基础组件,违纪记录表格、按钮、模态框全靠它 |
| animate.css | 页面过渡动画,首页和提示信息用 |
| font-awesome.min.css | 图标字体,按钮上的小图标 |
| responsive.css | 移动端适配,教师用手机查记录时不至于排版崩掉 |
| nivoslider.css / owl.carousel.css | 首页轮播图,放通知公告用 |
| magnific-popup.css | 弹窗组件,违纪详情预览用 |
| meanmenu.css | 移动端菜单折叠 |
后端选 Django 还有个隐性好处:Admin 后台可以直接当数据管理入口用。哪怕你不写一行前端代码,登录/admin就能增删改查违纪记录。对高校信息化这种“开发人力少、维护周期长”的场景,这是实打实的优势。数据库用 SQLite 起步完全够,等并发上来了再迁 MySQL,Django 的 ORM 层帮你挡掉了大部分迁移成本。
2.2 数据模型设计:学生、违纪记录、处罚记录三张表怎么建
整个系统能不能撑起查询和统计,取决于 models 怎么设计。我扒下来的核心模型是三个类:学生表、违纪记录表、处罚记录表,外加一个用户扩展表。学生表存姓名、学号、班级、学院,违纪记录表通过外键关联学生,记录违纪类型、时间、详情和登记人,处罚记录表再关联违纪记录,记录警告、记过、开除这类处理结果。这样设计的好处是:一次违纪可以对应多次处理跟进,处理状态能追溯。
# apps/affair/models.py from django.db import models from django.contrib.auth.models import User class Student(models.Model): name = models.CharField('姓名', max_length=50) student_no = models.CharField('学号', max_length=20, unique=True) class_name = models.CharField('班级', max_length=50) college = models.CharField('学院', max_length=100) phone = models.CharField('联系电话', max_length=20, blank=True) def __str__(self): return f'{self.name} ({self.student_no})' class Meta: db_table = 'student' verbose_name = '学生信息' class ViolationRecord(models.Model): VIOLATION_TYPES = [ ('late', '迟到'), ('absent', '旷课'), ('cheat', '考试作弊'), ('smoke', '吸烟'), ('fight', '打架斗殴'), ('other', '其他'), ] student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='violations', verbose_name='学生') violation_type = models.CharField('违纪类型', max_length=20, choices=VIOLATION_TYPES) violation_date = models.DateField('违纪日期') detail = models.TextField('违纪详情') recorder = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='登记人') created_at = models.DateTimeField('登记时间', auto_now_add=True) def __str__(self): return f'{self.student.name} - {self.get_violation_type_display()}' class Meta: db_table = 'violation_record' verbose_name = '违纪记录' ordering = ['-violation_date']学生表的student_no加了unique=True,防重复建档,这是违纪统计不出乱子的前提。违纪类型我用choices而不是单独的字典表——对于固定枚举值,choices 够用且查询简单,没必要再建一张表增加 JOIN。violation_date用DateField而不是DateTimeField,因为统计维度是按天聚合,时间字段越细越容易出时区偏差。related_name='violations'很关键,后面执行student.violations.filter(...)这种反向查询全靠它,你给这个参数起的名字,就是你写业务逻辑时用到的属性名。
处罚记录的模型类似,关键是把外键关联到ViolationRecord而不是Student,因为一次违纪可能有多轮处罚跟进(先警告、再记过),如果挂在 Student 下面就丢了“这次处罚针对哪次违纪”的上下文:
class PunishmentRecord(models.Model): PUNISHMENT_TYPES = [ ('warning', '警告'), ('demerit', '记过'), ('expel', '开除学籍'), ] violation = models.ForeignKey(ViolationRecord, on_delete=models.CASCADE, related_name='punishments', verbose_name='关联违纪') punishment_type = models.CharField('处罚类型', max_length=20, choices=PUNISHMENT_TYPES) reason = models.TextField('处罚原因') execute_date = models.DateField('执行日期') is_executed = models.BooleanField('是否已执行', default=False) class Meta: db_table = 'punishment_record' verbose_name = '处罚记录'on_delete=models.CASCADE表示删除学生时连带删除违纪记录,这是合理的,因为学生离校后这些记录没有独立保留价值。但处罚记录挂在违纪下面,删除违纪会连带删除处罚,这个要看学校制度——有些学校要求处罚记录留存多年,真上线时建议把CASCADE改成PROTECT,删不掉自然就不会误删。我在拆包时发现这类细节往往是答辩时老师最爱问的,也是实际部署时最容易埋雷的。
3. 违纪录入、条件查询与分类统计:ORM 查询与视图函数的落地写法
模型建好后,真正的业务逻辑集中在视图层。这一章直接看代码怎么写:录入怎么防重复提交、查询怎么组合条件、统计怎么用聚合函数一步到位。
3.1 违纪录入:表单校验与视图函数
录入页是教师和管理员最常用的入口。Django 的 ModelForm 可以直接基于ViolationRecord生成表单,省去手写 HTML 校验逻辑:
# apps/affair/forms.py from django import forms from .models import ViolationRecord class ViolationRecordForm(forms.ModelForm): class Meta: model = ViolationRecord fields = ['student', 'violation_type', 'violation_date', 'detail'] widgets = { 'violation_date': forms.DateInput(attrs={'type': 'date'}), 'detail': forms.Textarea(attrs={'rows': 4, 'placeholder': '请填写违纪具体经过'}), }# apps/affair/views.py from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required, permission_required from .forms import ViolationRecordForm @login_required @permission_required('affair.add_violationrecord', raise_exception=True) def add_violation(request): if request.method == 'POST': form = ViolationRecordForm(request.POST) if form.is_valid(): record = form.save(commit=False) record.recorder = request.user record.save() return redirect('affair:violation_list') else: form = ViolationRecordForm() return render(request, 'affair/violation_add.html', {'form': form})这里有两处容易翻车的地方。第一,commit=False是为了把当前登录用户塞进recorder字段,如果不这样写,recorder会为空,后面回溯“谁登记的”就查不到人。第二,permission_required的权限名affair.add_violationrecord是 Django 根据 app 名和模型名自动生成的,不能乱写——你在 Model 里把db_table改了不影响权限名,权限名始终是app_label.动作_模型名小写。如果提示权限不存在,检查一下 app 是不是注册在INSTALLED_APPS里,然后重新执行migrate。
3.2 条件查询:Q 对象组合姓名、类型、时间范围
查询页要支持多种条件自由组合:学生姓名或学号模糊搜、违纪类型下拉筛选、时间范围选择。如果一个个 if 拼接 filter 会很啰嗦,用 Q 对象可以把条件组装得干净利落:
from django.db.models import Q from django.core.paginator import Paginator from .models import ViolationRecord def violation_list(request): queryset = ViolationRecord.objects.select_related('student').all() keyword = request.GET.get('keyword', '').strip() violation_type = request.GET.get('violation_type', '') start_date = request.GET.get('start_date', '') end_date = request.GET.get('end_date', '') if keyword: queryset = queryset.filter( Q(student__name__icontains=keyword) | Q(student__student_no__icontains=keyword) ) if violation_type: queryset = queryset.filter(violation_type=violation_type) if start_date and end_date: queryset = queryset.filter(violation_date__range=[start_date, end_date]) paginator = Paginator(queryset, 15) page_obj = paginator.get_page(request.GET.get('page')) return render(request, 'affair/violation_list.html', {'page_obj': page_obj})Q(student__name__icontains=keyword)里双下划线是 Django ORM 跨表查询的约定写法,student__name表示关联到 Student 表的 name 字段,icontains表示大小写不敏感的模糊匹配。select_related('student')是性能关键——查询违纪列表时顺便把学生信息 JOIN 出来,避免每行数据都发一次 SQL 查学生表,这叫 N+1 查询问题,数据量到几百条时能明显感觉到卡顿。violation_date__range接收一个[开始日期, 结束日期]列表,注意结束日期当天的记录会被包含在内,因为 Django 会自动把日期转成当天的 23:59:59.999999,不用手动加一天。
3.3 分类统计:annotate + Count 按违纪类型和班级聚合
统计页是给教务处决策用的:哪个班级违纪多、哪种违纪类型高发。用annotate配合Count可以在数据库层面完成分组统计,而不是把数据取到 Python 里慢慢数:
from django.db.models import Count def violation_statistics(request): # 按违纪类型统计 type_stats = ( ViolationRecord.objects .values('violation_type') .annotate(total=Count('id')) .order_by('-total') ) # 按班级统计 class_stats = ( ViolationRecord.objects .values('student__class_name') .annotate(total=Count('id')) .order_by('-total') ) # 按违纪类型 + 班级组合统计 combo_stats = ( ViolationRecord.objects .values('student__class_name', 'violation_type') .annotate(total=Count('id')) .order_by('student__class_name', '-total') ) return render(request, 'affair/violation_statistics.html', { 'type_stats': list(type_stats), 'class_stats': list(class_stats), 'combo_stats': list(combo_stats), })values('violation_type')告诉 ORM 按这个字段分组,annotate(total=Count('id'))给每组加一个计数列。很多人会问:为什么 Count 用'id'而不是'*'?因为id是主键、非空且唯一,Count 主键和 Count 全部记录的结果完全一致,而且数据库对主键计数有优化,写'id'是最稳的。这三个统计结果直接以列表嵌套字典的形式返回给模板,前端用 Highcharts 或 ECharts 渲染成柱状图、饼图都行,不需要再写接口。
4. 报表生成与导出:把违纪统计倒成 Excel 和 PDF 的完整做法
系统做到能录入、能查询、能统计,已经能应付日常使用。但高校场景里,真正让老师离不开的是“导出”功能——学期末要交纸质材料、要给学院发通报、要在会议上展示数据。PDF 和 Excel 是两种最常见的交付格式,导出逻辑我分两条线写。
4.1 用 openpyxl 导出 Excel:表头、列宽与日期格式
Excel 导出选 openpyxl 而不是 csv 或 xlwt,原因是 openpyxl 能控制列宽、字体、单元格格式,导出的文件打开就是排版好的成品,不需要老师再手动调。核心代码如下:
# apps/affair/export.py from openpyxl import Workbook from openpyxl.styles import Font, Alignment from openpyxl.utils import get_column_letter from django.http import HttpResponse def export_violations_excel(queryset): wb = Workbook() ws = wb.active ws.title = '违纪记录' headers = ['学号', '姓名', '班级', '违纪类型', '违纪日期', '违纪详情', '登记人'] ws.append(headers) # 表头样式 for cell in ws[1]: cell.font = Font(bold=True) cell.alignment = Alignment(horizontal='center') for record in queryset: ws.append([ record.student.student_no, record.student.name, record.student.class_name, record.get_violation_type_display(), record.violation_date.strftime('%Y-%m-%d'), record.detail, record.recorder.username if record.recorder else '', ]) # 列宽自适应:中文按字符数估算 for col_idx in range(1, len(headers) + 1): col_letter = get_column_letter(col_idx) max_length = max( len(str(ws.cell(row=r, column=col_idx).value or '')) for r in range(1, ws.max_row + 1) ) ws.column_dimensions[col_letter].width = min(max_length * 1.2, 40) # 冻结首行 ws.freeze_panes = 'A2' response = HttpResponse( content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = 'attachment; filename="violations.xlsx"' wb.save(response) return response这段代码里有三个细节值得注意。第一,违纪类型字段存的是'late'、'absent'这样的英文键,导出时一定要用get_violation_type_display()换成中文,直接导出原始值是新手最容易犯的错。第二,列宽计算按字符数乘 1.2 并封顶 40,是因为中文在 Excel 里占两个英文字符宽度,乘 1.2 是经验值,封顶 40 防止超长详情把整列撑爆。第三,wb.save(response)是 openpyxl 直接把文件写入 HTTP 响应的标准做法,不需要先存临时文件再读出来。
4.2 用 weasyprint 导出 PDF:中文字体与分页处理
PDF 导出的坑比 Excel 多得多,主要集中在字体和分页上。weasyprint 是 Python 生态里把 HTML+CSS 渲染成 PDF 的成熟方案,好处是你可以用 Bootstrap 的样式直接排版报表,不用在代码里算坐标:
# apps/affair/export.py from django.template.loader import render_to_string from weasyprint import HTML def export_violations_pdf(queryset): html_string = render_to_string('affair/violation_pdf.html', { 'records': queryset, 'generated_at': timezone.now(), }) response = HttpResponse(content_type='application/pdf') response['Content-Disposition'] = 'attachment; filename="violations.pdf"' HTML(string=html_string).write_pdf(target=response) return response模板文件里的关键 CSS 是字体和分页控制:
<style> @page { size: A4; margin: 2cm; @bottom-center { content: "第 " counter(page) " 页 / 共 " counter(pages) " 页"; } } body { font-family: "Noto Sans CJK SC", "WenQuanYi Zen Hei", "Microsoft YaHei", sans-serif; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #333; padding: 6px 8px; font-size: 10pt; } thead { display: table-header-group; } tr { page-break-inside: avoid; } </style>font-family里按顺序写了 Linux 的 Noto Sans CJK、文泉驿正黑和 Windows 的微软雅黑——weasyprint 不会自动检测系统中文字体,如果你服务器上连中文字体都没装,导出 PDF 全是方框。我一般部署时先执行fc-list | grep -i cjk看一下服务器上有没有中文字体,没有就apt install fonts-noto-cjk装一套。thead { display: table-header-group; }是让多页表格每页都自动重复表头,没有这一行,翻到第二页就不知道哪列是什么了。page-break-inside: avoid防止单行记录被分页劈成两半。
导出 Excel 和 PDF 的按钮在页面上就是一个链接指向这两个视图,配合前端的 Bootstrap 按钮就行。这里我要多提醒一句:导出大范围数据时,比如一次性导出全校一学年的记录,最好加上异步任务——Celery 或 Django-Q 都行,否则浏览器转圈等几十秒,老师会以为系统崩了。小范围数据同步导出没问题,但这道坎得过。
5. 部署避坑:SQLite 迁移、MySQL 8 zip 配置与中文乱码的四个深坑
这套系统代码层面的坑不算多,真正折磨人的是部署环境。我按自己实际踩过的顺序,把最常见的四个问题写清楚,每条都是“现象 → 原因 → 解决”。
5.1 迁移翻车:新版 Django 不兼容 Python 3.8
现象:python manage.py migrate直接报ModuleNotFoundError: No module named 'django.core.exceptions'或一堆莫名其妙的语法错误,换 Python 版本又提示解释器不对。
原因:这套源码大概率是按 Django 4.x 写的,而很多人手头的 Python 还是 3.8(尤其学校机房预装环境)。新版 Django 5.x 要求 Python 3.10+,版本一错全盘崩。
解决:两个方向任选。要么把 Python 升到 3.10+,要么把 Django 降到 4.2 LTS(长期支持版,兼容 Python 3.8)。我一般建议先看requirements.txt里锁没锁版本,没锁就直接pip install "Django>=4.2,<5.0"。另外提醒一下 python 环境变量配置:Windows 下如果命令行里python指向的还是旧版本,去“系统属性 → 环境变量”把新 Python 的路径挪到Path最前面,改完要重新开终端才生效。
5.2 MySQL 8 zip 包配置:初始化与密码重置
现象:从 dev.mysql.com 下的 mysql-8.0-winx64.zip,解压后运行mysqld提示缺少 data 目录,服务装上了也起不来。
原因:zip 压缩包没有 MSI 安装向导,不会自动初始化数据目录和系统表,你得手动跑初始化命令。
解决:按顺序执行三步。第一步,在 MySQL 根目录下建个my.ini(网上能搜到模板,关键配置是basedir和datadir指向你的解压路径);第二步,以管理员身份打开 CMD,执行mysqld --initialize-insecure,这一步会生成 data 目录且 root 账号初始无密码;第三步,mysqld --install注册 Windows 服务,然后net start mysql。如果忘记密码了,用--skip-grant-tables启动后再ALTER USER重置。这套流程和这套违纪系统没直接关系,但十个想用 MySQL 的人里有八个栽在这个 zip 包上,干脆写出来。
5.3 静态文件 404:CSS 全部加载失败
现象:页面能打开但全是裸 HTML,bootstrap.min.css、style.css 一个都加载不出来,F12 看请求全是 404。
原因:Django 开发模式下runserver会自动托管静态文件,但只要settings.py里DEBUG = False(部署时必关),静态文件就得交给 Nginx 或白名单机制处理。
解决:最快的方式是装whitenoise——一个中间件,让 Django 自己托管静态文件,小项目完全够用。pip install whitenoise,然后在MIDDLEWARE里加一行whitenoise.middleware.WhiteNoiseMiddleware,再执行python manage.py collectstatic把所有 app 的静态资源汇总到STATIC_ROOT,就完事了。注意STATIC_ROOT路径不要和STATICFILES_DIRS重叠,否则 collectstatic 会报错。
5.4 PDF 导出中文全是方框
现象:Excel 导出正常,PDF 导出中文全变成“□□□”。
原因:weasyprint 渲染 PDF 时依赖系统字体,服务器上没装中文字体,它只能拿默认字体凑合。
解决:前面第 4 章已经提过,这里再补一个操作细节——在violation_pdf.html的 CSS 里把字体路径写成绝对路径更稳,比如@font-face直接指向C:\Windows\Fonts\msyh.ttc(Windows 部署)或/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc(Linux 部署)。有同事跟我犟说font-family写名字就行,结果在 CentOS 上翻了车——系统里根本没装对应的 fontconfig 配置,写名字找不到,写绝对路径反而省事。字体问题属于那种“一次配好、以后再也不想碰”的典型坑,我建议上线前把字体检查和更新一起写进部署脚本里。
6. 预警机制进阶:用 Django signal 实现多次违纪自动提醒
前面把录入、查询、统计、导出都覆盖了,这套系统已经能正常交付。但摘要里提到的“多次违纪预警”还没落地。这个功能如果写在视图里,只有走录入页面才会触发判断,万一管理员在 Admin 后台直接录数据,预警就失效了。所以我建议用 Django 的 signal 机制——在模型保存的瞬间触发判断,不管数据从哪个入口进来的都拦不住。
# apps/affair/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone from datetime import timedelta from .models import ViolationRecord @receiver(post_save, sender=ViolationRecord) def check_high_frequency_violation(sender, instance, created, **kwargs): if not created: return # 统计近 30 天内该学生的违纪次数 start_date = timezone.localdate() - timedelta(days=30) count = ViolationRecord.objects.filter( student=instance.student, violation_date__gte=start_date ).count() # 次数达到或超过 3 次,在处罚记录表里自动生成一条提醒 if count >= 3: PunishmentRecord.objects.get_or_create( violation=instance, defaults={ 'punishment_type': 'warning', 'reason': f'近30天内违纪{count}次,系统自动预警', 'execute_date': timezone.localdate(), } )信号函数接收的instance是刚保存的记录,created=True表示这是新建而不是修改。统计近 30 天次数时用的是violation_date__gte而不是created_at__gte,因为违纪日期可能早于登记日期,统计要以违纪事实发生时间为准。最后用get_or_create避免信号重复执行时生成多条预警记录。
为了让信号生效,还要在 app 的配置类里导入它:
# apps/affair/apps.py from django.apps import AppConfig class AffairConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'apps.affair' def ready(self): import apps.affair.signals # 注册信号ready()方法是 Django 在启动时回调的钩子,在这里导入信号模块,信号才会被注册到全局。忘记写这一步是新手最常见的失误——代码看着没问题,但预警就是不触发,因为信号根本没加载。
这个预警逻辑跑通后,你还可以顺势做一层优化:给Student模型加一个attention_level字段,预警触发时把学生标记为“重点关注”,列表页用颜色高亮。从那以后我每次做类似的师生管理系统,都强制自己在模型保存层做校验和提醒,而不是堆在视图里——视图会被绕开,signals 才是最后一个关卡。希望帮到你。
本文还有配套的精品资源,点击获取