资讯详情 Python Django高校违纪管理系统:数据模型、报表导出与部署实战
📅 2026/10/26 23:41:49
简介一套基于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 起步完全够等并发上来了再迁 MySQLDjango 的 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_length50) student_no models.CharField(学号, max_length20, uniqueTrue) class_name models.CharField(班级, max_length50) college models.CharField(学院, max_length100) phone models.CharField(联系电话, max_length20, blankTrue) 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_deletemodels.CASCADE, related_nameviolations, verbose_name学生) violation_type models.CharField(违纪类型, max_length20, choicesVIOLATION_TYPES) violation_date models.DateField(违纪日期) detail models.TextField(违纪详情) recorder models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name登记人) created_at models.DateTimeField(登记时间, auto_now_addTrue) 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加了uniqueTrue防重复建档这是违纪统计不出乱子的前提。违纪类型我用choices而不是单独的字典表——对于固定枚举值choices 够用且查询简单没必要再建一张表增加 JOIN。violation_date用DateField而不是DateTimeField因为统计维度是按天聚合时间字段越细越容易出时区偏差。related_nameviolations很关键后面执行student.violations.filter(...)这种反向查询全靠它你给这个参数起的名字就是你写业务逻辑时用到的属性名。处罚记录的模型类似关键是把外键关联到ViolationRecord而不是Student因为一次违纪可能有多轮处罚跟进先警告、再记过如果挂在 Student 下面就丢了“这次处罚针对哪次违纪”的上下文class PunishmentRecord(models.Model): PUNISHMENT_TYPES [ (warning, 警告), (demerit, 记过), (expel, 开除学籍), ] violation models.ForeignKey(ViolationRecord, on_deletemodels.CASCADE, related_namepunishments, verbose_name关联违纪) punishment_type models.CharField(处罚类型, max_length20, choicesPUNISHMENT_TYPES) reason models.TextField(处罚原因) execute_date models.DateField(执行日期) is_executed models.BooleanField(是否已执行, defaultFalse) class Meta: db_table punishment_record verbose_name 处罚记录on_deletemodels.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_exceptionTrue) def add_violation(request): if request.method POST: form ViolationRecordForm(request.POST) if form.is_valid(): record form.save(commitFalse) record.recorder request.user record.save() return redirect(affair:violation_list) else: form ViolationRecordForm() return render(request, affair/violation_add.html, {form: form})这里有两处容易翻车的地方。第一commitFalse是为了把当前登录用户塞进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__icontainskeyword) | Q(student__student_no__icontainskeyword) ) if violation_type: queryset queryset.filter(violation_typeviolation_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__icontainskeyword)里双下划线是 Django ORM 跨表查询的约定写法student__name表示关联到 Student 表的 name 字段icontains表示大小写不敏感的模糊匹配。select_related(student)是性能关键——查询违纪列表时顺便把学生信息 JOIN 出来避免每行数据都发一次 SQL 查学生表这叫 N1 查询问题数据量到几百条时能明显感觉到卡顿。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(totalCount(id)) .order_by(-total) ) # 按班级统计 class_stats ( ViolationRecord.objects .values(student__class_name) .annotate(totalCount(id)) .order_by(-total) ) # 按违纪类型 班级组合统计 combo_stats ( ViolationRecord.objects .values(student__class_name, violation_type) .annotate(totalCount(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(totalCount(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(boldTrue) cell.alignment Alignment(horizontalcenter) 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(rowr, columncol_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_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet ) response[Content-Disposition] attachment; filenameviolations.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 生态里把 HTMLCSS 渲染成 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_typeapplication/pdf) response[Content-Disposition] attachment; filenameviolations.pdf HTML(stringhtml_string).write_pdf(targetresponse) 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; } /stylefont-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 Django4.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 静态文件 404CSS 全部加载失败现象页面能打开但全是裸 HTMLbootstrap.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.ttcWindows 部署或/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttcLinux 部署。有同事跟我犟说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, senderViolationRecord) def check_high_frequency_violation(sender, instance, created, **kwargs): if not created: return # 统计近 30 天内该学生的违纪次数 start_date timezone.localdate() - timedelta(days30) count ViolationRecord.objects.filter( studentinstance.student, violation_date__gtestart_date ).count() # 次数达到或超过 3 次在处罚记录表里自动生成一条提醒 if count 3: PunishmentRecord.objects.get_or_create( violationinstance, defaults{ punishment_type: warning, reason: f近30天内违纪{count}次系统自动预警, execute_date: timezone.localdate(), } )信号函数接收的instance是刚保存的记录createdTrue表示这是新建而不是修改。统计近 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 才是最后一个关卡。希望帮到你。本文还有配套的精品资源点击获取